motion_core/instances/
analytic.rs1use super::{InstanceKind, MAX_INSTANCE_COUNT};
4use crate::ValidationError;
5use std::sync::Arc;
6
7pub const ANALYTIC_HEADER_WORDS: usize = 20;
9
10pub const ANALYTIC_STATE_WORDS: usize = 8;
12
13const MAX_ANALYTIC_SECONDS: f32 = 3600.0;
15
16const MAX_ANALYTIC_VALUE: f32 = 1e6;
18
19const MAX_ANALYTIC_PHASE: f32 = 8192.0;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24#[repr(u32)]
25pub enum AnalyticTrajectoryKind {
26 Ballistic = 1,
28 Converge,
30 Orbit,
32 NoiseDrift,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct AnalyticInstanceState {
39 pub position: [f32; 2],
41 pub velocity: [f32; 2],
43 pub custom: [f32; 2],
45 pub index: u32,
47 pub rotation: f32,
49}
50
51#[derive(Clone, Debug, PartialEq)]
57pub struct AnalyticInstanceUpload {
58 words: Arc<[f32]>,
59}
60
61impl AnalyticInstanceUpload {
62 #[expect(
73 clippy::too_many_arguments,
74 reason = "The versioned model has explicit time, seed and primitive inputs"
75 )]
76 pub fn new(
77 kind: AnalyticTrajectoryKind,
78 seed: u64,
79 age: f32,
80 lifetime: f32,
81 parameters: [f32; 8],
82 primitive: InstanceKind,
83 align_to_velocity: bool,
84 states: impl ExactSizeIterator<Item = AnalyticInstanceState>,
85 ) -> Result<Self, ValidationError> {
86 let invalid = || {
87 ValidationError::invalid(
88 "instances.analytic",
89 "program",
90 "Invalid analytic model, time, initial state or precision range",
91 )
92 };
93 let count = states.len();
94 if count > MAX_INSTANCE_COUNT
95 || !age.is_finite()
96 || !lifetime.is_finite()
97 || !(0.0..=MAX_ANALYTIC_SECONDS).contains(&age)
98 || lifetime <= 0.0
99 || lifetime > MAX_ANALYTIC_SECONDS
100 || age >= lifetime
101 || parameters.iter().any(|value| !value.is_finite())
102 || !matches!(
103 primitive,
104 InstanceKind::Disc | InstanceKind::Glow | InstanceKind::Sprite
105 )
106 || (kind == AnalyticTrajectoryKind::Ballistic
107 && (parameters[4] < 0.0 || parameters[6] < 0.0 || parameters[7] < parameters[6]))
108 || (kind == AnalyticTrajectoryKind::Orbit && parameters[2] < 0.0)
109 || (kind == AnalyticTrajectoryKind::NoiseDrift
110 && (parameters[4] < 0.0 || parameters[5] < 0.0))
111 {
112 return Err(invalid());
113 }
114 let precision = || {
115 ValidationError::unsupported(
116 "instances.analytic",
117 "precision",
118 "Trajectory exceeds the bounded f32 analytic precision range",
119 )
120 };
121 if parameters
122 .iter()
123 .any(|value| value.abs() > MAX_ANALYTIC_VALUE)
124 || (kind == AnalyticTrajectoryKind::Orbit
125 && (parameters[3] * age).abs() > MAX_ANALYTIC_PHASE)
126 || (kind == AnalyticTrajectoryKind::NoiseDrift
127 && parameters[5] * age > MAX_ANALYTIC_PHASE)
128 {
129 return Err(precision());
130 }
131 let expected = ANALYTIC_HEADER_WORDS + count * ANALYTIC_STATE_WORDS;
132 let mut words = Vec::new();
133 words.try_reserve_exact(expected).map_err(|_| {
134 ValidationError::limit(
135 "instances.analytic",
136 "max_bytes",
137 "Could not reserve analytic upload",
138 )
139 })?;
140 words.extend([
141 kind as u32 as f32,
142 age,
143 lifetime,
144 primitive as u32 as f32,
145 u32::from(align_to_velocity) as f32,
146 count as f32,
147 f32::from_bits(seed as u32),
148 f32::from_bits((seed >> 32) as u32),
149 ]);
150 words.extend(parameters);
151 words.extend([0.0; 4]);
152 for state in states {
153 if words.len() >= expected
154 || state
155 .position
156 .into_iter()
157 .chain(state.velocity)
158 .chain(state.custom)
159 .chain([state.rotation])
160 .any(|value| !value.is_finite())
161 {
162 return Err(invalid());
163 }
164 if state
165 .position
166 .into_iter()
167 .chain(state.velocity)
168 .chain(state.custom)
169 .chain([state.rotation])
170 .any(|value| value.abs() > MAX_ANALYTIC_VALUE)
171 || (kind == AnalyticTrajectoryKind::NoiseDrift
172 && state
173 .custom
174 .iter()
175 .any(|value| value.abs() > MAX_ANALYTIC_PHASE))
176 {
177 return Err(precision());
178 }
179 words.extend(state.position);
180 words.extend(state.velocity);
181 words.extend(state.custom);
182 words.extend([f32::from_bits(state.index), state.rotation]);
183 }
184 if words.len() != expected {
185 return Err(invalid());
186 }
187 Ok(Self {
188 words: words.into(),
189 })
190 }
191
192 pub fn len(&self) -> usize {
194 self.words[5] as usize
195 }
196
197 pub fn is_empty(&self) -> bool {
199 self.len() == 0
200 }
201
202 pub fn bytes(&self) -> u64 {
204 self.words.len() as u64 * 4
205 }
206
207 pub fn words(&self) -> &[f32] {
209 &self.words
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn analytic_upload_validates_time_finiteness_and_exact_layout() {
219 let state = AnalyticInstanceState {
220 position: [0.0; 2],
221 velocity: [1.0; 2],
222 custom: [0.0; 2],
223 index: u32::MAX,
224 rotation: 0.0,
225 };
226 let make = |age, parameters, state| {
227 AnalyticInstanceUpload::new(
228 AnalyticTrajectoryKind::Ballistic,
229 u64::MAX,
230 age,
231 2.0,
232 parameters,
233 InstanceKind::Disc,
234 false,
235 [state].into_iter(),
236 )
237 };
238 let upload = make(1.0, [0.0; 8], state).unwrap();
239 assert_eq!(upload.len(), 1);
240 assert_eq!(
241 upload.bytes(),
242 ((ANALYTIC_HEADER_WORDS + ANALYTIC_STATE_WORDS) * 4) as u64
243 );
244 assert_eq!(upload.words()[6].to_bits(), u32::MAX);
245 assert!(make(2.0, [0.0; 8], state).is_err());
246 assert!(make(f32::NAN, [0.0; 8], state).is_err());
247 assert!(make(1.0, [f32::INFINITY; 8], state).is_err());
248 assert!(
249 make(
250 1.0,
251 [0.0; 8],
252 AnalyticInstanceState {
253 rotation: f32::NAN,
254 ..state
255 }
256 )
257 .is_err()
258 );
259 }
260}