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}