Skip to main content

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}