1use async_channel::{Receiver, bounded};
14use unlit_ecs::{Bundle, DynamicBundle, Entity, EntityAccessError, World, bundle};
15
16use crate::channel::publish_collected;
17use crate::executor::LocalTasks;
18use crate::handle::WorldHandle;
19use crate::job::{Job, JobSender, SendError};
20use crate::thread::{ThreadControl, ThreadStatus, WorldThread, panic_message, stepped_control};
21
22struct WorldProxy {
29 kind: WorldKind,
31}
32
33enum WorldKind {
35 Threaded {
37 _thread: WorldThread,
39 },
40 Stepped(Box<SteppedWorld>),
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct WorldName(pub &'static str);
51
52#[derive(Clone)]
61pub struct WorldSender {
62 sender: JobSender,
64 status: ThreadStatus,
66}
67
68impl WorldSender {
69 pub fn send(&self, job: Job) -> Result<(), SendError> {
71 self.sender.send(job)
72 }
73
74 pub fn is_finished(&self) -> bool {
79 self.status.is_finished()
80 }
81}
82
83type Tick = Box<dyn FnMut(&mut World, &ThreadControl, f32) -> bool>;
85
86struct SteppedWorld {
91 world: World,
93 inbox: Receiver<Job>,
95 control: ThreadControl,
97 status: ThreadStatus,
99 tick: Tick,
101 name: &'static str,
103}
104
105impl Drop for SteppedWorld {
106 fn drop(&mut self) {
107 self.status.finish();
112 self.inbox.close();
113 }
114}
115
116pub trait WorldRunner: 'static {
123 const SELF_DRIVEN: bool;
128
129 fn spawn<I, S, T>(
131 host: &mut World,
132 name: &'static str,
133 command_capacity: usize,
134 init: I,
135 tick: T,
136 ) -> WorldHandle
137 where
138 I: FnOnce(&mut World) -> S + Send + 'static,
139 S: 'static,
140 T: FnMut(&mut World, &ThreadControl, &mut S, f32) -> bool + Send + 'static;
141}
142
143pub struct Threaded;
145
146pub struct Stepped;
148
149impl WorldRunner for Threaded {
150 const SELF_DRIVEN: bool = true;
151
152 fn spawn<I, S, T>(
153 host: &mut World,
154 name: &'static str,
155 command_capacity: usize,
156 init: I,
157 tick: T,
158 ) -> WorldHandle
159 where
160 I: FnOnce(&mut World) -> S + Send + 'static,
161 S: 'static,
162 T: FnMut(&mut World, &ThreadControl, &mut S, f32) -> bool + Send + 'static,
163 {
164 spawn_threaded_world_with_init(host, name, command_capacity, init, tick)
165 }
166}
167
168impl WorldRunner for Stepped {
169 const SELF_DRIVEN: bool = false;
170
171 fn spawn<I, S, T>(
172 host: &mut World,
173 name: &'static str,
174 command_capacity: usize,
175 init: I,
176 tick: T,
177 ) -> WorldHandle
178 where
179 I: FnOnce(&mut World) -> S + Send + 'static,
180 S: 'static,
181 T: FnMut(&mut World, &ThreadControl, &mut S, f32) -> bool + Send + 'static,
182 {
183 spawn_stepped_world_with_init(host, name, command_capacity, init, tick)
184 }
185}
186
187pub fn spawn_threaded_world<T>(
203 host: &mut World,
204 name: &'static str,
205 command_capacity: usize,
206 mut tick: T,
207) -> WorldHandle
208where
209 T: FnMut(&mut World, &ThreadControl, f32) -> bool + Send + 'static,
210{
211 spawn_threaded_world_with_init(
212 host,
213 name,
214 command_capacity,
215 |_| (),
216 move |world, control, (), delta_seconds| tick(world, control, delta_seconds),
217 )
218}
219
220pub fn spawn_threaded_world_with_init<I, S, T>(
232 host: &mut World,
233 name: &'static str,
234 command_capacity: usize,
235 init: I,
236 mut tick: T,
237) -> WorldHandle
238where
239 I: FnOnce(&mut World) -> S + Send + 'static,
240 S: 'static,
241 T: FnMut(&mut World, &ThreadControl, &mut S, f32) -> bool + Send + 'static,
242{
243 let (queue, inbox) = bounded::<Job>(command_capacity);
244 let thread = WorldThread::spawn_with(name, init, move |world, control, state| {
245 while let Ok(job) = inbox.try_recv() {
246 job(world);
247 }
248 let running = tick(world, control, state, control.elapsed_seconds());
249 publish_collected(world);
252 running
253 });
254 let status: ThreadStatus = thread.status();
255 let sender = JobSender::new(queue);
256 let port = WorldSender {
257 sender: sender.clone(),
258 status: status.clone(),
259 };
260 let entity = host.root_scope_mut().spawn(WorldEntity {
261 kind: WorldKind::Threaded { _thread: thread },
262 name,
263 sender: port,
264 });
265 WorldHandle::with_status(entity, sender, status)
266}
267
268pub fn spawn_stepped_world_with_init<I, S, T>(
281 host: &mut World,
282 name: &'static str,
283 command_capacity: usize,
284 init: I,
285 mut tick: T,
286) -> WorldHandle
287where
288 I: FnOnce(&mut World) -> S + 'static,
289 S: 'static,
290 T: FnMut(&mut World, &ThreadControl, &mut S, f32) -> bool + 'static,
291{
292 let (queue, inbox) = bounded::<Job>(command_capacity);
293 let mut world = World::new();
294 LocalTasks::install(&mut world);
297 let mut state = init(&mut world);
298 let (control, status) = stepped_control();
299 let tick = Box::new(
300 move |world: &mut World, control: &ThreadControl, delta_seconds: f32| {
301 tick(world, control, &mut state, delta_seconds)
302 },
303 );
304 let stepped = SteppedWorld {
305 world,
306 inbox,
307 control,
308 status: status.clone(),
309 tick,
310 name,
311 };
312 let sender = JobSender::new(queue);
313 let port = WorldSender {
314 sender: sender.clone(),
315 status: status.clone(),
316 };
317 let entity = host.root_scope_mut().spawn(WorldEntity {
318 kind: WorldKind::Stepped(Box::new(stepped)),
319 name,
320 sender: port,
321 });
322 WorldHandle::with_status(entity, sender, status)
323}
324
325struct WorldEntity {
328 kind: WorldKind,
330 name: &'static str,
332 sender: WorldSender,
334}
335
336impl Bundle for WorldEntity {
337 fn name(&self) -> &'static str {
338 self.name
339 }
340
341 fn into_bundle(self) -> DynamicBundle {
342 bundle![
343 WorldProxy { kind: self.kind },
344 WorldName(self.name),
345 self.sender,
346 ]
347 }
348}
349
350#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
352#[non_exhaustive]
353pub enum StepError {
354 #[error("cannot reach the world proxy: {0}")]
356 NoWorld(#[from] EntityAccessError),
357 #[error("the world is threaded and drives itself")]
359 NotStepped,
360}
361
362#[derive(Clone, Copy, Debug, PartialEq, Eq)]
364#[non_exhaustive]
365pub enum StepOutcome {
366 Ran,
368 Finished,
370 Stopped,
372 Panicked,
374}
375
376pub fn step_stepped_world(
392 host: &World,
393 entity: Entity,
394 delta_seconds: f32,
395) -> Result<StepOutcome, StepError> {
396 let mut proxy = host.root_scope().query_one::<&mut WorldProxy>(entity)?;
397 let WorldKind::Stepped(stepped) = &mut proxy.get().kind else {
398 return Err(StepError::NotStepped);
399 };
400 if stepped.status.is_finished() {
401 return Ok(StepOutcome::Finished);
402 }
403 if stepped.control.stop_requested() {
404 publish_collected(&stepped.world);
407 stepped.status.finish();
408 stepped.inbox.close();
409 return Ok(StepOutcome::Stopped);
410 }
411 if !run_queued_jobs(&mut *stepped) {
412 return Ok(StepOutcome::Panicked);
413 }
414 let SteppedWorld {
415 world,
416 inbox,
417 control,
418 tick,
419 status,
420 name,
421 } = &mut **stepped;
422 LocalTasks::pump(world);
423 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
424 tick(world, control, delta_seconds)
425 }));
426 match outcome {
427 Ok(true) => {}
428 Ok(false) => {
429 publish_collected(world);
432 status.finish();
433 inbox.close();
434 return Ok(StepOutcome::Finished);
435 }
436 Err(payload) => {
437 let message = panic_message(payload.as_ref());
438 log::error!("the stepped world {name} panicked: {message}");
439 status.record_panic(message);
440 inbox.close();
443 return Ok(StepOutcome::Panicked);
444 }
445 }
446 publish_collected(world);
447 Ok(StepOutcome::Ran)
448}
449
450pub fn drain_all_stepped_worlds(host: &World) {
456 let mut proxies = host.root_scope().query::<&mut WorldProxy>();
457 for proxy in proxies.iter() {
458 let WorldKind::Stepped(stepped) = &mut proxy.kind else {
459 continue;
460 };
461 if stepped.status.is_finished() || stepped.control.stop_requested() {
462 continue;
463 }
464 run_queued_jobs(stepped);
465 }
466}
467
468fn run_queued_jobs(stepped: &mut SteppedWorld) -> bool {
470 loop {
471 let Ok(job) = stepped.inbox.try_recv() else {
472 return true;
473 };
474 let outcome =
475 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| job(&mut stepped.world)));
476 if let Err(payload) = outcome {
477 let message = panic_message(payload.as_ref());
478 log::error!("the stepped world {} panicked: {message}", stepped.name);
479 stepped.status.record_panic(message);
480 stepped.inbox.close();
484 return false;
485 }
486 }
487}
488
489pub fn with_stepped_world<R>(
500 host: &World,
501 entity: Entity,
502 f: impl FnOnce(&World) -> R,
503) -> Result<R, StepError> {
504 let mut proxy = host.root_scope().query_one::<&WorldProxy>(entity)?;
505 let WorldKind::Stepped(stepped) = &proxy.get().kind else {
506 return Err(StepError::NotStepped);
507 };
508 Ok(f(&stepped.world))
509}
510
511pub fn with_stepped_world_mut<R>(
521 host: &World,
522 entity: Entity,
523 f: impl FnOnce(&mut World) -> R,
524) -> Result<R, StepError> {
525 let mut proxy = host.root_scope().query_one::<&mut WorldProxy>(entity)?;
526 let WorldKind::Stepped(stepped) = &mut proxy.get().kind else {
527 return Err(StepError::NotStepped);
528 };
529 Ok(f(&mut stepped.world))
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::FRAME_SECONDS;
536 use crate::job::SendError;
537 use std::time::Duration;
538 use unlit_ecs::bundle;
539
540 #[test]
543 fn sent_jobs_run_and_publish_back() {
544 let mut host = World::new();
545 let handle = spawn_threaded_world(&mut host, "test", 8, |_, control, _| {
546 control.wait_timeout(Duration::from_millis(1));
547 true
548 });
549 handle
550 .sender
551 .send(Box::new(|world: &mut World| {
552 world.root_scope_mut().spawn(bundle![Count(0)]);
553 }))
554 .expect("the world accepts jobs");
555 let counted = crate::channel::Snapshot::new();
556 let reader = counted.reader();
557 let publish = counted.clone();
558 handle
559 .sender
560 .send(Box::new(move |world: &mut World| {
561 world
562 .root_scope()
563 .query::<&mut Count>()
564 .iter()
565 .next()
566 .expect("the counter")
567 .0 += 1;
568 let mut count_rows = world.root_scope().query::<&Count>();
569
570 let count = count_rows.iter().next().expect("the counter").0;
571 publish.publish(count);
572 }))
573 .expect("the world accepts jobs");
574 let mut answer = None;
575 for _ in 0..2_000 {
576 answer = reader.latest().map(|count| *count);
577 if answer.is_some() {
578 break;
579 }
580 std::thread::sleep(Duration::from_millis(1));
581 }
582 assert_eq!(answer, Some(1));
583 }
584
585 #[test]
587 fn init_runs_before_the_first_job() {
588 let mut host = World::new();
589 let handle = spawn_threaded_world_with_init(
590 &mut host,
591 "test",
592 8,
593 |world: &mut World| {
594 world.root_scope_mut().spawn(bundle![Count(0)]);
595 },
596 |_, control, (), _| {
597 control.wait_timeout(Duration::from_millis(1));
598 true
599 },
600 );
601 let seeded = crate::channel::Snapshot::new();
602 let reader = seeded.reader();
603 let publish = seeded.clone();
604 handle
605 .sender
606 .send(Box::new(move |world: &mut World| {
607 publish.publish(world.root_scope().query::<&Count>().iter().next().is_some());
608 }))
609 .expect("the world accepts jobs");
610 let mut answered = None;
611 for _ in 0..2_000 {
612 answered = reader.latest().map(|found| *found);
613 if answered.is_some() {
614 break;
615 }
616 std::thread::sleep(Duration::from_millis(1));
617 }
618 assert_eq!(answered, Some(true), "init ran before the job");
619 }
620
621 #[test]
623 fn a_full_queue_fails_the_send() {
624 let mut host = World::new();
625 let handle = spawn_threaded_world(&mut host, "test", 1, |_, _, _| true);
626 let mut results = Vec::new();
627 for _ in 0..64 {
628 results.push(handle.sender.send(Box::new(|_| {})));
629 }
630 assert!(
631 results.contains(&Err(SendError::Full)),
632 "a bounded queue must eventually report Full"
633 );
634 }
635
636 #[test]
638 fn a_stepped_world_runs_on_its_owner_s_calls() {
639 let mut host = World::new();
640 let handle = spawn_stepped_world_with_init(
641 &mut host,
642 "stepped",
643 8,
644 |world: &mut World| {
645 world.root_scope_mut().spawn(bundle![Count(0)]);
646 },
647 |world: &mut World, _control: &ThreadControl, (), _: f32| {
648 world
649 .root_scope()
650 .query::<&mut Count>()
651 .iter()
652 .next()
653 .expect("the counter")
654 .0 += 1;
655 true
656 },
657 );
658 let count = |host: &World| {
659 with_stepped_world(host, handle.entity, |world| {
660 world
661 .root_scope()
662 .query::<&Count>()
663 .iter()
664 .next()
665 .expect("the counter")
666 .0
667 })
668 };
669 assert_eq!(count(&host), Ok(0));
671 assert_eq!(
672 step_stepped_world(&host, handle.entity, FRAME_SECONDS),
673 Ok(StepOutcome::Ran)
674 );
675 assert_eq!(count(&host), Ok(1));
676 assert_eq!(
677 step_stepped_world(&host, handle.entity, FRAME_SECONDS),
678 Ok(StepOutcome::Ran)
679 );
680 assert_eq!(count(&host), Ok(2));
681 }
682
683 #[test]
686 fn draining_all_stepped_worlds_does_not_conflict_with_its_own_query() {
687 let mut host = World::new();
688 let first = spawn_stepped_world_with_init(
689 &mut host,
690 "first",
691 8,
692 |world: &mut World| {
693 world.root_scope_mut().spawn(bundle![Count(0)]);
694 },
695 |_, _, (), _| true,
696 );
697 let second = spawn_stepped_world_with_init(
698 &mut host,
699 "second",
700 8,
701 |world: &mut World| {
702 world.root_scope_mut().spawn(bundle![Count(0)]);
703 },
704 |_, _, (), _| true,
705 );
706 for handle in [&first, &second] {
707 handle
708 .sender
709 .send(Box::new(|world: &mut World| {
710 world
711 .root_scope()
712 .query::<&mut Count>()
713 .iter()
714 .next()
715 .expect("the counter")
716 .0 += 1;
717 }))
718 .expect("the world accepts jobs");
719 }
720 drain_all_stepped_worlds(&host);
722 for handle in [&first, &second] {
723 let count = with_stepped_world(&host, handle.entity, |world| {
724 world
725 .root_scope()
726 .query::<&Count>()
727 .iter()
728 .next()
729 .expect("the counter")
730 .0
731 });
732 assert_eq!(count, Ok(1), "the queued job ran");
733 }
734 }
735
736 #[test]
738 fn a_stepped_tick_ends_the_world() {
739 let mut host = World::new();
740 let handle = spawn_stepped_world_with_init(
741 &mut host,
742 "stepped",
743 8,
744 |_| (),
745 |_, _: &ThreadControl, (), _: f32| false,
746 );
747 assert_eq!(
748 step_stepped_world(&host, handle.entity, FRAME_SECONDS),
749 Ok(StepOutcome::Finished)
750 );
751 assert!(handle.is_finished(), "a false tick ends a stepped world");
752 }
753
754 #[test]
756 fn stepping_a_foreign_entity_fails_readably() {
757 let mut host = World::new();
758 let plain = host.root_scope_mut().spawn(bundle![Count(0)]);
759 assert!(matches!(
760 step_stepped_world(&host, plain, 0.0),
761 Err(StepError::NoWorld(_))
762 ));
763 }
764
765 #[test]
768 fn stepping_a_threaded_world_is_refused() {
769 let mut host = World::new();
770 let handle = spawn_threaded_world(&mut host, "threaded", 8, |_, control, _| {
771 control.wait_timeout(Duration::from_millis(1));
772 true
773 });
774 assert_eq!(
775 step_stepped_world(&host, handle.entity, FRAME_SECONDS),
776 Err(StepError::NotStepped)
777 );
778 handle.stop();
779 }
780
781 struct Count(u32);
783}