1use super::*;
4
5mod replay;
6
7#[derive(Clone, Copy, Debug)]
9pub struct SimulationLimits {
10 pub max_particles: usize,
12 pub max_bytes: u64,
14 pub max_steps: u64,
16 pub max_work: u64,
18 pub max_sub_depth: u8,
20 pub max_births: u64,
22 pub max_events: usize,
24}
25
26impl Default for SimulationLimits {
27 fn default() -> Self {
28 Self {
29 max_particles: 16384,
30 max_bytes: 64 * 1024 * 1024,
31 max_steps: 1_000_000,
32 max_work: 100_000_000,
33 max_sub_depth: 8,
34 max_births: 1_000_000,
35 max_events: 65536,
36 }
37 }
38}
39
40#[derive(Clone, Copy, Debug)]
42pub struct SimulationStats {
43 pub snapshots: usize,
45 pub interval: u64,
47 pub admitted_bytes: u64,
49 pub work: u64,
51}
52
53#[derive(Clone)]
57pub struct ParticleSimulation {
58 config: Arc<Config>,
59 pub(crate) style: Arc<ParticleStyle>,
60
61 prepared: Arc<OnceLock<Result<Prepared, ValidationError>>>,
62}
63
64impl ParticleSimulation {
65 pub fn new(seed: u64) -> Self {
68 Self {
69 config: Arc::new(Config {
70 seed,
71 duration: None,
72 step: Time::from_ticks(motion_core::TICKS_PER_SECOND / 240),
73 interval: 60,
74 emitters: Vec::new(),
75 modifiers: Vec::new(),
76 groups: Vec::new(),
77 sub_emitters: Vec::new(),
78 prewarm: Time::ZERO,
79 loop_period: None,
80 time_scale: 1.0,
81 time_offset: Time::ZERO,
82 allow_reverse: false,
83 workers: 1,
84 limits: SimulationLimits::default(),
85 }),
86 style: Arc::new(ParticleStyle::default()),
87 prepared: Arc::new(OnceLock::new()),
88 }
89 }
90
91 fn change(mut self, update: impl FnOnce(&mut Config)) -> Self {
92 update(Arc::make_mut(&mut self.config));
93 self.prepared = Arc::new(OnceLock::new());
94 self
95 }
96
97 pub fn duration(self, duration: Duration) -> Self {
99 self.change(|config| config.duration = Some(duration))
100 }
101
102 pub fn step(self, step: Duration) -> Self {
104 self.change(|config| config.step = step)
105 }
106
107 pub fn snapshot_interval(self, steps: u64) -> Self {
109 self.change(|config| config.interval = steps)
110 }
111
112 pub fn limits(self, limits: SimulationLimits) -> Self {
114 self.change(|config| config.limits = limits)
115 }
116
117 pub fn emitter(self, emitter: impl ParticleEmitter + 'static) -> Self {
119 self.change(|config| config.emitters.push(Arc::new(emitter)))
120 }
121
122 pub fn modifier(self, modifier: impl ParticleModifier + 'static) -> Self {
124 self.change(|config| config.modifiers.push(Arc::new(modifier)))
125 }
126
127 pub fn group_modifier(self, modifier: impl ParticleGroupModifier + 'static) -> Self {
129 self.change(|config| config.groups.push(Arc::new(modifier)))
130 }
131
132 pub fn sub_emitter(self, emitter: SubEmitter) -> Self {
134 self.change(|config| config.sub_emitters.push(emitter))
135 }
136
137 pub fn prewarm(self, duration: Duration) -> Self {
139 self.change(|config| config.prewarm = duration)
140 }
141
142 pub fn loop_period(self, period: Duration) -> Self {
144 self.change(|config| config.loop_period = Some(period))
145 }
146
147 pub fn time_remap(self, scale: f64, offset: Duration, allow_reverse: bool) -> Self {
149 self.change(|config| {
150 config.time_scale = scale;
151 config.time_offset = offset;
152 config.allow_reverse = allow_reverse;
153 })
154 }
155
156 pub fn workers(self, workers: usize) -> Self {
158 self.change(|config| config.workers = workers)
159 }
160
161 fn prepared(&self, duration: Option<Duration>) -> Result<&Prepared, ValidationError> {
162 let duration = self.config.duration.or(duration).ok_or_else(|| {
163 invalid(
164 "duration",
165 "Provide a simulation duration or a finite clip duration",
166 )
167 })?;
168 let prepared = self
169 .prepared
170 .get_or_init(|| prepare(&self.config, duration))
171 .as_ref()
172 .map_err(Clone::clone)?;
173 if prepared.duration != duration {
174 return Err(invalid(
175 "duration",
176 "Cached simulation duration changed; use a distinct cache key/instance",
177 ));
178 }
179 Ok(prepared)
180 }
181
182 pub fn sample(&self, time: Time) -> Result<Arc<[Particle]>, ValidationError> {
185 self.sample_in(time, None)
186 }
187
188 pub fn sample_in(
190 &self,
191 time: Time,
192 clip_duration: Option<Duration>,
193 ) -> Result<Arc<[Particle]>, ValidationError> {
194 self.sample_with(&mut SampleCursor::default(), time, clip_duration)
195 }
196
197 pub(crate) fn sample_with(
200 &self,
201 cursor: &mut SampleCursor,
202 time: Time,
203 clip_duration: Option<Duration>,
204 ) -> Result<Arc<[Particle]>, ValidationError> {
205 let prepared = self.prepared(clip_duration)?;
206 if time < Time::ZERO || time >= prepared.duration {
207 return Ok(Arc::from([]));
208 }
209 if let Some(replay) = &prepared.replay {
210 return replay.sample(&self.config, &mut cursor.replay, time);
211 }
212 let target = time.ticks() as u64 / self.config.step.ticks() as u64;
213 let index = prepared
214 .snapshots
215 .partition_point(|snapshot| snapshot.step <= target)
216 - 1;
217 let checkpoint = &prepared.snapshots[index];
218 if cursor
219 .state
220 .as_ref()
221 .is_some_and(|state| state.step > target || state.step < checkpoint.step)
222 {
223 cursor.state = None;
224 }
225 let state = cursor.state.get_or_insert_with(|| checkpoint.clone());
226 let mut work = 0;
227 while state.step < target {
228 if let Err(error) = advance(&self.config, state, &mut work) {
229 cursor.state = None;
231 return Err(error);
232 }
233 }
234 Ok(state
235 .live
236 .iter()
237 .map(|live| live.particle)
238 .collect::<Vec<_>>()
239 .into())
240 }
241
242 pub fn stats(&self) -> Result<SimulationStats, ValidationError> {
244 Ok(self.prepared(None)?.stats)
245 }
246
247 pub fn events(&self, time: Time) -> Result<Vec<ParticleEvent>, ValidationError> {
249 let prepared = self.prepared(None)?;
250 if time < Time::ZERO || time >= prepared.duration {
251 return Ok(Vec::new());
252 }
253 if let Some(replay) = &prepared.replay {
254 replay.events(&self.config, time)
255 } else {
256 Ok(Vec::new())
257 }
258 }
259
260 pub(super) fn natural_duration(&self) -> Option<Duration> {
261 self.config.duration
262 }
263}
264
265#[derive(Clone)]
266struct Config {
267 seed: u64,
268 duration: Option<Duration>,
269 step: Duration,
270 interval: u64,
271 emitters: Vec<Arc<dyn ParticleEmitter>>,
272 modifiers: Vec<Arc<dyn ParticleModifier>>,
273
274 groups: Vec<Arc<dyn ParticleGroupModifier>>,
275 sub_emitters: Vec<SubEmitter>,
276 prewarm: Duration,
277 loop_period: Option<Duration>,
278 time_scale: f64,
279 time_offset: Duration,
280 allow_reverse: bool,
281 workers: usize,
282
283 limits: SimulationLimits,
284}
285
286#[derive(Clone, Copy)]
287struct Live {
288 particle: Particle,
289 birth: Time,
290}
291
292#[derive(Clone)]
293struct State {
294 step: u64,
295 next: u64,
296 live: Vec<Live>,
297}
298
299struct Prepared {
300 duration: Duration,
301 snapshots: Vec<State>,
302 replay: Option<replay::PreparedReplay>,
303 stats: SimulationStats,
304}
305
306#[derive(Default)]
311pub(crate) struct SampleCursor {
312 state: Option<State>,
313 replay: replay::ReplayCursor,
314}
315
316fn charge(config: &Config, work: &mut u64, amount: usize) -> Result<(), ValidationError> {
317 *work = work
318 .checked_add(amount as u64)
319 .ok_or_else(|| limit("Simulation work overflow"))?;
320 if *work > config.limits.max_work {
321 return Err(limit("Simulation work allowance exhausted"));
322 }
323 Ok(())
324}
325
326fn emitter_random(config: &Config, order: usize, step: u64) -> Random {
328 Random::for_particle(config.seed ^ 0xe11e0000 ^ order as u64, step, 0)
329}
330
331fn modifier_random(config: &Config, order: usize, index: u64, step: u64) -> Random {
333 Random::for_particle(
334 config.seed ^ (order as u64).wrapping_mul(0xd1b54a32d192ed03),
335 index,
336 step,
337 )
338}
339
340fn births(
341 config: &Config,
342 state: &mut State,
343 time: Time,
344 work: &mut u64,
345) -> Result<(), ValidationError> {
346 for (index, emitter) in config.emitters.iter().enumerate() {
347 let mut output = Vec::new();
348 let mut random = emitter_random(config, index, state.step);
349 emitter.emit(state.step, time, &mut random, &mut output)?;
350 charge(config, work, output.len() + 1)?;
351 if output.len() > config.limits.max_particles.saturating_sub(state.live.len()) {
352 return Err(limit("Live particle allowance exhausted"));
353 }
354 for mut particle in output {
355 if state.next >= 1_000_000 {
356 return Err(limit("Simulation exceeds one million total births"));
357 }
358 particle.index = state.next;
359 particle.age = 0.0;
360 particle.validate()?;
361 state.next += 1;
362 state.live.push(Live {
363 particle,
364 birth: time,
365 });
366 }
367 }
368 Ok(())
369}
370
371fn advance(config: &Config, state: &mut State, work: &mut u64) -> Result<(), ValidationError> {
372 state.step += 1;
373 let time = Time::from_ticks(state.step as i64 * config.step.ticks());
374 let seconds = config.step.as_seconds() as f32;
375 charge(
376 config,
377 work,
378 state.live.len() * (config.modifiers.len() + 2),
379 )?;
380 for live in &mut state.live {
381 let particle = &mut live.particle;
382 particle.age = (time - live.birth).as_seconds() as f32;
383 if particle.age >= particle.lifetime {
384 continue;
385 }
386 let index = particle.index;
387 let age = particle.age;
388 for (order, modifier) in config.modifiers.iter().enumerate() {
389 let mut random = modifier_random(config, order, index, state.step);
390 modifier.apply(particle, seconds, time, &mut random)?;
391 particle.index = index;
392 particle.age = age;
393 particle.validate()?;
394 }
395 for axis in 0..2 {
396 particle.position[axis] += particle.velocity[axis] * seconds;
397 }
398 particle.validate()?;
399 }
400 state
401 .live
402 .retain(|live| live.particle.age < live.particle.lifetime);
403 births(config, state, time, work)
404}
405
406fn prepare(config: &Config, duration: Duration) -> Result<Prepared, ValidationError> {
407 if replay::enabled(config) {
408 let replay = replay::prepare(config, duration)?;
409 let stats = replay.stats;
410 return Ok(Prepared {
411 duration,
412 snapshots: Vec::new(),
413 replay: Some(replay),
414 stats,
415 });
416 }
417 let limits = config.limits;
418 if duration <= Time::ZERO
419 || duration.as_seconds() > 3600.0
420 || config.step <= Time::ZERO
421 || config.step > duration
422 || config.interval == 0
423 || limits.max_particles == 0
424 || limits.max_particles > 65536
425 || limits.max_work == 0
426 || config.emitters.len() > 32
427 || config.modifiers.len() > 32
428 {
429 return Err(invalid(
430 "configuration",
431 "Invalid simulation duration/step/components/limits",
432 ));
433 }
434 let steps = duration.ticks() as u64 / config.step.ticks() as u64;
435 if steps > limits.max_steps || steps > 1_000_000 {
436 return Err(limit("Simulation fixed-step allowance exhausted"));
437 }
438 let slot = limits.max_particles as u64 * std::mem::size_of::<Live>() as u64
439 + std::mem::size_of::<State>() as u64;
440 let slots = limits.max_bytes / slot;
441 if slots < 3 {
442 return Err(limit(
443 "Snapshot budget needs one snapshot and two bounded working buffers",
444 ));
445 }
446 let capacity = slots - 2;
447 let interval = if capacity == 1 {
448 steps + 1
449 } else {
450 config.interval.max(steps.div_ceil(capacity - 1))
451 };
452 let mut state = State {
453 step: 0,
454 next: 0,
455 live: Vec::new(),
456 };
457 let mut work = 0;
458 births(config, &mut state, Time::ZERO, &mut work)?;
459 let mut snapshots = vec![state.clone()];
460 while state.step < steps {
461 advance(config, &mut state, &mut work)?;
462 if state.step.is_multiple_of(interval) {
463 snapshots.push(state.clone());
464 }
465 }
466 let stats = SimulationStats {
467 snapshots: snapshots.len(),
468 interval,
469 admitted_bytes: (snapshots.len() as u64 + 2) * slot,
470 work,
471 };
472 Ok(Prepared {
473 duration,
474 snapshots,
475 replay: None,
476 stats,
477 })
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use motion_core::TimeLiteral;
484 use std::sync::atomic::{AtomicUsize, Ordering};
485
486 struct Birth(Arc<AtomicUsize>);
487
488 impl ParticleEmitter for Birth {
489 fn emit(
490 &self,
491 step: u64,
492 _: Time,
493 _: &mut Random,
494 output: &mut Vec<Particle>,
495 ) -> Result<(), ValidationError> {
496 self.0.fetch_add(1, Ordering::SeqCst);
497 if step == 0 {
498 output.push(Particle {
499 velocity: [1.0, 0.0],
500 lifetime: 2.0,
501 ..Default::default()
502 });
503 }
504 Ok(())
505 }
506 }
507
508 struct Acceleration;
509
510 impl ParticleModifier for Acceleration {
511 fn apply(
512 &self,
513 particle: &mut Particle,
514 step: f32,
515 _: Time,
516 _: &mut Random,
517 ) -> Result<(), ValidationError> {
518 particle.velocity[1] += 2.0 * step;
519 Ok(())
520 }
521 }
522
523 #[test]
524 fn fixed_snapshot_replay_is_order_independent_once_initialized_and_bounded()
525 -> motion_core::Result<()> {
526 assert_eq!(std::mem::size_of::<Particle>(), 72);
527 let calls = Arc::new(AtomicUsize::new(0));
528 let simulation = ParticleSimulation::new(42)
529 .duration(1.seconds())
530 .step(10.milliseconds())
531 .emitter(Birth(calls.clone()))
532 .modifier(Acceleration)
533 .limits(SimulationLimits {
534 max_particles: 2,
535 max_bytes: 2048,
536 ..Default::default()
537 })
538 .snapshot_interval(1);
539 std::thread::scope(|scope| {
540 for _ in 0..4 {
541 let simulation = &simulation;
542 scope.spawn(move || {
543 let point = simulation.sample(500.milliseconds()).unwrap()[0];
544 assert!((point.position[0] - 0.5).abs() < 1e-6);
545 assert!((point.position[1] - 0.255).abs() < 1e-6);
546 });
547 }
548 });
549 assert_eq!(calls.load(Ordering::SeqCst), 101 + 4 * 5);
550 let stats = simulation.stats()?;
551 assert!(stats.interval > 1 && stats.admitted_bytes <= 2048);
552 for time in [990, 0, 500, 40, 500] {
553 assert_eq!(
554 simulation.sample(time.milliseconds())?,
555 simulation.sample(time.milliseconds())?
556 );
557 }
558 assert!(simulation.sample(1.seconds())?.is_empty());
559 assert!(
560 simulation
561 .clone()
562 .limits(SimulationLimits {
563 max_bytes: 1,
564 ..Default::default()
565 })
566 .sample(Time::ZERO)
567 .is_err()
568 );
569 Ok(())
570 }
571
572 #[test]
573 fn cursor_queries_match_independent_samples_for_every_replay_path() -> motion_core::Result {
574 let plain = ParticleSimulation::new(9)
575 .duration(2.seconds())
576 .emitter(
577 crate::particles::PointEmitter::new(motion_core::Point::ORIGIN)
578 .speed(3.0)
579 .lifetime(700.milliseconds()),
580 )
581 .modifier(Acceleration);
582 let simulations = [
583 plain.clone(),
584 plain.clone().workers(2),
585 plain.loop_period(900.milliseconds()),
586 ];
587 let times = [
589 0, 10, 240, 250, 251, 300, 300, 1200, 1490, 260, 270, 1999, 5, 2000, 1000,
590 ];
591 for simulation in &simulations {
592 let mut cursor = SampleCursor::default();
593 for time in times {
594 let time = Time::from_ticks(motion_core::TICKS_PER_SECOND * time / 1000);
595 assert_eq!(
596 &*simulation.sample_with(&mut cursor, time, None)?,
597 &*simulation.sample_in(time, None)?,
598 "{time:?}"
599 );
600 }
601 }
602 Ok(())
603 }
604}