concinnity_core/physics/fanout.rs
1// How a host lends the simulation its threads.
2//
3// `concinnity_physics::Fanout` is generic in the item and the body so a step's
4// work units are monomorphised into it rather than boxed, which is what a
5// system holding one as a trait object cannot be. So the seam is drawn one
6// level up: a host answers "advance this simulation by dt", and the fan-out it
7// reaches for on the way stays inside its own answer.
8//
9// The ordering contract is not this seam's to keep. A step hands out
10// independent work and loads every result back in manifold order afterwards,
11// so which thread solved what is not observable in the world state either way;
12// what a fan-out decides is only how long the step takes.
13
14use concinnity_physics::{Inline, Simulation};
15
16use crate::ecs::ScheduleMode;
17
18/// A host's way of lending the simulation the threads it has.
19///
20/// A world with none runs its steps on the calling thread through
21/// [`SerialFanout`], which is the default a [`PhysicsSystem`] is built with.
22///
23/// [`PhysicsSystem`]: crate::physics::PhysicsSystem
24pub trait PhysicsFanout: core::fmt::Debug + Send {
25 /// Workers the step's per-worker scratch is reserved for under `mode`.
26 /// Read once, at world start.
27 fn worker_count(&self, mode: ScheduleMode) -> usize;
28
29 /// Advance `sim` by `dt`, lending it whatever `mode` names.
30 fn step(&self, sim: &mut Simulation, dt: f32, mode: ScheduleMode);
31}
32
33/// The fan-out for a host with no threads to lend: every step runs on the
34/// calling thread.
35#[derive(Debug, Clone, Copy, Default)]
36pub struct SerialFanout;
37
38impl PhysicsFanout for SerialFanout {
39 fn worker_count(&self, _mode: ScheduleMode) -> usize {
40 1
41 }
42
43 fn step(&self, sim: &mut Simulation, dt: f32, _mode: ScheduleMode) {
44 sim.step_with(dt, &Inline);
45 }
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51 use concinnity_physics::{ColliderShape, DynamicParams, LayerMask};
52
53 fn falling() -> (Simulation, concinnity_physics::BodyHandle) {
54 let mut sim = Simulation::with_capacity(4);
55 let ball = sim
56 .add_dynamic(
57 &ColliderShape::Ball { radius: 0.5 },
58 [0.0, 10.0, 0.0],
59 [0.0; 3],
60 DynamicParams {
61 mass: 1.0,
62 friction: 0.5,
63 restitution: 0.0,
64 gravity_scale: 1.0,
65 linear_damping: 0.0,
66 },
67 LayerMask::ALL,
68 )
69 .expect("room for one body");
70 (sim, ball)
71 }
72
73 // The serial fan-out reserves one worker's scratch and advances the world,
74 // which is the whole of what a host with no pool needs from it.
75 #[test]
76 fn the_serial_fanout_lends_one_worker_and_steps() {
77 assert_eq!(SerialFanout.worker_count(ScheduleMode::Serial), 1);
78 assert_eq!(SerialFanout.worker_count(ScheduleMode::Parallel), 1);
79
80 let (mut sim, ball) = falling();
81 for _ in 0..10 {
82 SerialFanout.step(&mut sim, 1.0 / 60.0, ScheduleMode::Serial);
83 }
84 assert!(
85 sim.body_pose_quat(ball).expect("a live body").0[1] < 10.0,
86 "the fan-out advanced the simulation"
87 );
88 }
89
90 // The mode a serial fan-out is asked for changes nothing: it has one place
91 // to run the step, so both land on identical state.
92 #[test]
93 fn the_schedule_mode_does_not_change_a_serial_step() {
94 let run = |mode| {
95 let (mut sim, ball) = falling();
96 for _ in 0..30 {
97 SerialFanout.step(&mut sim, 1.0 / 60.0, mode);
98 }
99 sim.body_pose_quat(ball).expect("a live body").0
100 };
101 assert_eq!(run(ScheduleMode::Serial), run(ScheduleMode::Parallel));
102 }
103}