Skip to main content

dynamis_world/dynamics/
capacity.rs

1use dynamis_layout::{
2    COUNTER_ENTRIES, COUNTER_EVENTS, COUNTER_PAIRS, COUNTER_SPILLOVER_ENTRIES,
3    COUNTER_SPILLOVER_EVENTS, COUNTER_SPILLOVER_PAIRS, COUNTER_SPILLOVER_RESTING, Counters,
4    MAX_CELLS_PER_COLLIDER,
5};
6use dynamis_model::MAX_COLLIDERS_PER_BODY;
7
8const SLOTS_HEADROOM: u32 = 2;
9const COMMANDS_PER_BODY: u32 = 4;
10const MOVE_ENTRIES_PER_COMMAND: u32 = 2;
11const CONSTRAINT_COMMANDS_PER_CONSTRAINT: u32 = 4;
12const QUERIES_PER_BODY: u32 = 2;
13const MIN_SLOTS: u32 = 64;
14const STREAM_DENSITY_PAIRS: u32 = 16;
15const STREAM_DENSITY_EVENTS: u32 = 8;
16
17pub(crate) const STREAM_FLOOR: u32 = 256;
18
19pub(crate) struct Live {
20    pub(crate) bodies: u32,
21    pub(crate) constraints: u32,
22    pub(crate) body_commands: u32,
23    pub(crate) constraint_commands: u32,
24    pub(crate) queries: u32,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub(crate) struct StreamDemand {
29    pub(crate) pairs: u32,
30    pub(crate) entries: u32,
31    pub(crate) events: u32,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub(crate) enum CapacityPlan {
36    Widen(StreamDemand),
37    Narrow(StreamDemand),
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub(crate) struct Reservation {
42    pub(crate) bodies: u32,
43    pub(crate) constraints: u32,
44    pub(crate) entries: u32,
45    pub(crate) pairs: u32,
46    pub(crate) events: u32,
47    pub(crate) body_commands: u32,
48    pub(crate) constraint_commands: u32,
49    pub(crate) queries: u32,
50}
51
52fn product(left: u32, right: u32, name: &str) -> u32 {
53    left.checked_mul(right)
54        .unwrap_or_else(|| panic!("{name} capacity exceeds the device index space"))
55}
56
57fn grown(current: u32, live: u32) -> u32 {
58    if live <= current {
59        return current.max(MIN_SLOTS);
60    }
61    live.max(current.saturating_mul(SLOTS_HEADROOM))
62        .max(MIN_SLOTS)
63}
64
65fn narrowed(current: u32, live: u32) -> u32 {
66    let target = live.max(MIN_SLOTS).saturating_mul(SLOTS_HEADROOM);
67    current.min(current.saturating_div(2).max(target))
68}
69
70fn stream_grown(current: u32, required: u32, demand: u32) -> u32 {
71    current.max(required).max(demand).max(STREAM_FLOOR)
72}
73
74fn stream_narrowed(current: u32, target: u32) -> u32 {
75    let half = current.saturating_div(2);
76    current.min(target.max(half).max(STREAM_FLOOR))
77}
78
79impl Reservation {
80    pub(crate) fn initial() -> Self {
81        Self::planned(
82            &Self {
83                bodies: 0,
84                constraints: 0,
85                entries: 0,
86                pairs: 0,
87                events: 0,
88                body_commands: 0,
89                constraint_commands: 0,
90                queries: 0,
91            },
92            &Live {
93                bodies: 0,
94                constraints: 0,
95                body_commands: 0,
96                constraint_commands: 0,
97                queries: 0,
98            },
99            None,
100        )
101    }
102
103    pub(crate) fn planned(current: &Self, live: &Live, plan: Option<CapacityPlan>) -> Self {
104        let widen = plan
105            .as_ref()
106            .is_none_or(|plan| matches!(plan, CapacityPlan::Widen(_)));
107        let bodies = if widen {
108            grown(current.bodies, live.bodies)
109        } else {
110            narrowed(current.bodies, live.bodies)
111        };
112        let constraints = if widen {
113            grown(current.constraints, live.constraints)
114        } else {
115            narrowed(current.constraints, live.constraints)
116        };
117        let demand = match plan {
118            Some(CapacityPlan::Widen(demand)) | Some(CapacityPlan::Narrow(demand)) => demand,
119            None => StreamDemand {
120                pairs: 0,
121                entries: 0,
122                events: 0,
123            },
124        };
125
126        let collider_rows = product(live.bodies, MAX_COLLIDERS_PER_BODY as u32, "collider");
127        let entry_budget = product(collider_rows, MAX_CELLS_PER_COLLIDER, "grid entry");
128        let pair_budget = product(live.bodies, STREAM_DENSITY_PAIRS, "pair");
129        let event_budget = product(live.bodies, STREAM_DENSITY_EVENTS, "event");
130
131        let entries = if widen {
132            stream_grown(current.entries, entry_budget, demand.entries)
133        } else {
134            stream_narrowed(current.entries, entry_budget.max(demand.entries))
135        };
136        let pairs = if widen {
137            stream_grown(current.pairs, pair_budget, demand.pairs)
138        } else {
139            stream_narrowed(current.pairs, pair_budget.max(demand.pairs))
140        };
141        let events = if widen {
142            stream_grown(current.events, event_budget, demand.events)
143        } else {
144            stream_narrowed(current.events, event_budget.max(demand.events))
145        };
146        let body_commands = if widen {
147            stream_grown(
148                current.body_commands,
149                live.body_commands
150                    .max(product(bodies, COMMANDS_PER_BODY, "body command")),
151                0,
152            )
153        } else {
154            stream_narrowed(
155                current.body_commands,
156                live.body_commands
157                    .max(product(bodies, COMMANDS_PER_BODY, "body command")),
158            )
159        };
160        let constraint_commands = if widen {
161            stream_grown(
162                current.constraint_commands,
163                live.constraint_commands.max(product(
164                    constraints,
165                    CONSTRAINT_COMMANDS_PER_CONSTRAINT,
166                    "constraint command",
167                )),
168                0,
169            )
170        } else {
171            stream_narrowed(
172                current.constraint_commands,
173                live.constraint_commands.max(product(
174                    constraints,
175                    CONSTRAINT_COMMANDS_PER_CONSTRAINT,
176                    "constraint command",
177                )),
178            )
179        };
180        let queries = if widen {
181            stream_grown(
182                current.queries,
183                live.queries.max(product(bodies, QUERIES_PER_BODY, "query")),
184                0,
185            )
186        } else {
187            stream_narrowed(
188                current.queries,
189                live.queries.max(product(bodies, QUERIES_PER_BODY, "query")),
190            )
191        };
192        Self {
193            bodies,
194            constraints,
195            entries,
196            pairs,
197            events,
198            body_commands,
199            constraint_commands,
200            queries,
201        }
202    }
203
204    pub(crate) fn streams(&self) -> StreamCapacity {
205        StreamCapacity {
206            entries: self.entries,
207            pairs: self.pairs,
208            events: self.events,
209        }
210    }
211
212    pub(crate) fn colliders(&self) -> u32 {
213        product(self.bodies, MAX_COLLIDERS_PER_BODY as u32, "collider")
214    }
215
216    pub(crate) fn body_moves(&self) -> u32 {
217        product(
218            self.body_commands,
219            MOVE_ENTRIES_PER_COMMAND,
220            "body row move",
221        )
222    }
223
224    pub(crate) fn constraint_moves(&self) -> u32 {
225        product(
226            self.constraint_commands,
227            MOVE_ENTRIES_PER_COMMAND,
228            "constraint row move",
229        )
230    }
231
232    pub(crate) fn sort(&self) -> u32 {
233        self.entries.max(self.pairs).max(self.blocks())
234    }
235
236    pub(crate) fn blocks(&self) -> u32 {
237        self.pairs.saturating_add(self.constraints)
238    }
239}
240
241#[derive(Clone, Copy, Debug, PartialEq, Eq)]
242pub struct StreamCapacity {
243    pub entries: u32,
244    pub pairs: u32,
245    pub events: u32,
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249pub(crate) struct ShapeReservation {
250    pub(crate) sources: u32,
251    pub(crate) vertices: u32,
252    pub(crate) triangles: u32,
253    pub(crate) nodes: u32,
254}
255
256impl ShapeReservation {
257    pub(crate) const EMPTY: Self = Self {
258        sources: 0,
259        vertices: 0,
260        triangles: 0,
261        nodes: 0,
262    };
263
264    pub(crate) fn planned(current: &Self, used: &Self) -> Self {
265        Self {
266            sources: grown(current.sources, used.sources),
267            vertices: grown(current.vertices, used.vertices),
268            triangles: grown(current.triangles, used.triangles),
269            nodes: grown(current.nodes, used.nodes),
270        }
271    }
272}
273
274const PRESSURE_HEADROOM: u32 = 8;
275
276const IDLE_FRACTION: u32 = 4;
277
278const IDLE_DELAY: u32 = 120;
279
280const PLAN_COOLDOWN: u32 = 10;
281
282const STREAM_HEADROOM: u32 = 2;
283
284#[derive(Clone, Copy)]
285struct StreamWatch {
286    pressure: bool,
287    idle_steps: u32,
288}
289
290impl StreamWatch {
291    const IDLE: Self = Self {
292        pressure: false,
293        idle_steps: 0,
294    };
295
296    fn observe(&mut self, demand: u32, spilled: bool, lanes: u32) {
297        let free = lanes.saturating_sub(demand);
298        if spilled || free < lanes / PRESSURE_HEADROOM {
299            self.pressure = true;
300            self.idle_steps = 0;
301        } else if demand * IDLE_FRACTION < lanes {
302            self.idle_steps = self.idle_steps.saturating_add(1);
303        } else {
304            self.idle_steps = 0;
305        }
306    }
307}
308
309pub(crate) struct Capacity {
310    pairs: StreamWatch,
311    entries: StreamWatch,
312    events: StreamWatch,
313    cooldown: u32,
314}
315
316impl Default for Capacity {
317    fn default() -> Self {
318        Self::new()
319    }
320}
321
322impl Capacity {
323    pub(crate) const fn new() -> Self {
324        Self {
325            pairs: StreamWatch::IDLE,
326            entries: StreamWatch::IDLE,
327            events: StreamWatch::IDLE,
328            cooldown: 0,
329        }
330    }
331
332    pub(crate) fn observe(
333        &mut self,
334        measured: &Counters,
335        plan: &Reservation,
336    ) -> Option<CapacityPlan> {
337        self.pairs.observe(
338            measured[COUNTER_PAIRS],
339            measured[COUNTER_SPILLOVER_PAIRS] > 0 || measured[COUNTER_SPILLOVER_RESTING] > 0,
340            plan.pairs,
341        );
342        self.entries.observe(
343            measured[COUNTER_ENTRIES],
344            measured[COUNTER_SPILLOVER_ENTRIES] > 0,
345            plan.entries,
346        );
347        self.events.observe(
348            measured[COUNTER_EVENTS],
349            measured[COUNTER_SPILLOVER_EVENTS] > 0,
350            plan.events,
351        );
352        self.cooldown = self.cooldown.saturating_sub(1);
353        let under_pressure = (self.pairs.pressure || self.entries.pressure || self.events.pressure)
354            && self.cooldown == 0;
355        let idle = self.cooldown == 0
356            && self.pairs.idle_steps >= IDLE_DELAY
357            && self.entries.idle_steps >= IDLE_DELAY
358            && self.events.idle_steps >= IDLE_DELAY;
359        if !under_pressure && !idle {
360            return None;
361        }
362        self.pairs = StreamWatch::IDLE;
363        self.entries = StreamWatch::IDLE;
364        self.events = StreamWatch::IDLE;
365        self.cooldown = PLAN_COOLDOWN;
366        let demand = StreamDemand {
367            pairs: serve(measured[COUNTER_PAIRS]),
368            entries: serve(measured[COUNTER_ENTRIES]),
369            events: serve(measured[COUNTER_EVENTS]),
370        };
371        if under_pressure {
372            Some(CapacityPlan::Widen(demand))
373        } else {
374            Some(CapacityPlan::Narrow(demand))
375        }
376    }
377}
378
379fn serve(demand: u32) -> u32 {
380    demand.saturating_mul(STREAM_HEADROOM).max(STREAM_FLOOR)
381}