use super::{InstanceKind, MAX_INSTANCE_COUNT};
use crate::ValidationError;
use std::sync::Arc;
pub const ANALYTIC_HEADER_WORDS: usize = 20;
pub const ANALYTIC_STATE_WORDS: usize = 8;
const MAX_ANALYTIC_SECONDS: f32 = 3600.0;
const MAX_ANALYTIC_VALUE: f32 = 1e6;
const MAX_ANALYTIC_PHASE: f32 = 8192.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum AnalyticTrajectoryKind {
Ballistic = 1,
Converge,
Orbit,
NoiseDrift,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnalyticInstanceState {
pub position: [f32; 2],
pub velocity: [f32; 2],
pub custom: [f32; 2],
pub index: u32,
pub rotation: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AnalyticInstanceUpload {
words: Arc<[f32]>,
}
impl AnalyticInstanceUpload {
#[expect(
clippy::too_many_arguments,
reason = "The versioned model has explicit time, seed and primitive inputs"
)]
pub fn new(
kind: AnalyticTrajectoryKind,
seed: u64,
age: f32,
lifetime: f32,
parameters: [f32; 8],
primitive: InstanceKind,
align_to_velocity: bool,
states: impl ExactSizeIterator<Item = AnalyticInstanceState>,
) -> Result<Self, ValidationError> {
let invalid = || {
ValidationError::invalid(
"instances.analytic",
"program",
"Invalid analytic model, time, initial state or precision range",
)
};
let count = states.len();
if count > MAX_INSTANCE_COUNT
|| !age.is_finite()
|| !lifetime.is_finite()
|| !(0.0..=MAX_ANALYTIC_SECONDS).contains(&age)
|| lifetime <= 0.0
|| lifetime > MAX_ANALYTIC_SECONDS
|| age >= lifetime
|| parameters.iter().any(|value| !value.is_finite())
|| !matches!(
primitive,
InstanceKind::Disc | InstanceKind::Glow | InstanceKind::Sprite
)
|| (kind == AnalyticTrajectoryKind::Ballistic
&& (parameters[4] < 0.0 || parameters[6] < 0.0 || parameters[7] < parameters[6]))
|| (kind == AnalyticTrajectoryKind::Orbit && parameters[2] < 0.0)
|| (kind == AnalyticTrajectoryKind::NoiseDrift
&& (parameters[4] < 0.0 || parameters[5] < 0.0))
{
return Err(invalid());
}
let precision = || {
ValidationError::unsupported(
"instances.analytic",
"precision",
"Trajectory exceeds the bounded f32 analytic precision range",
)
};
if parameters
.iter()
.any(|value| value.abs() > MAX_ANALYTIC_VALUE)
|| (kind == AnalyticTrajectoryKind::Orbit
&& (parameters[3] * age).abs() > MAX_ANALYTIC_PHASE)
|| (kind == AnalyticTrajectoryKind::NoiseDrift
&& parameters[5] * age > MAX_ANALYTIC_PHASE)
{
return Err(precision());
}
let expected = ANALYTIC_HEADER_WORDS + count * ANALYTIC_STATE_WORDS;
let mut words = Vec::new();
words.try_reserve_exact(expected).map_err(|_| {
ValidationError::limit(
"instances.analytic",
"max_bytes",
"Could not reserve analytic upload",
)
})?;
words.extend([
kind as u32 as f32,
age,
lifetime,
primitive as u32 as f32,
u32::from(align_to_velocity) as f32,
count as f32,
f32::from_bits(seed as u32),
f32::from_bits((seed >> 32) as u32),
]);
words.extend(parameters);
words.extend([0.0; 4]);
for state in states {
if words.len() >= expected
|| state
.position
.into_iter()
.chain(state.velocity)
.chain(state.custom)
.chain([state.rotation])
.any(|value| !value.is_finite())
{
return Err(invalid());
}
if state
.position
.into_iter()
.chain(state.velocity)
.chain(state.custom)
.chain([state.rotation])
.any(|value| value.abs() > MAX_ANALYTIC_VALUE)
|| (kind == AnalyticTrajectoryKind::NoiseDrift
&& state
.custom
.iter()
.any(|value| value.abs() > MAX_ANALYTIC_PHASE))
{
return Err(precision());
}
words.extend(state.position);
words.extend(state.velocity);
words.extend(state.custom);
words.extend([f32::from_bits(state.index), state.rotation]);
}
if words.len() != expected {
return Err(invalid());
}
Ok(Self {
words: words.into(),
})
}
pub fn len(&self) -> usize {
self.words[5] as usize
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn bytes(&self) -> u64 {
self.words.len() as u64 * 4
}
pub fn words(&self) -> &[f32] {
&self.words
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn analytic_upload_validates_time_finiteness_and_exact_layout() {
let state = AnalyticInstanceState {
position: [0.0; 2],
velocity: [1.0; 2],
custom: [0.0; 2],
index: u32::MAX,
rotation: 0.0,
};
let make = |age, parameters, state| {
AnalyticInstanceUpload::new(
AnalyticTrajectoryKind::Ballistic,
u64::MAX,
age,
2.0,
parameters,
InstanceKind::Disc,
false,
[state].into_iter(),
)
};
let upload = make(1.0, [0.0; 8], state).unwrap();
assert_eq!(upload.len(), 1);
assert_eq!(
upload.bytes(),
((ANALYTIC_HEADER_WORDS + ANALYTIC_STATE_WORDS) * 4) as u64
);
assert_eq!(upload.words()[6].to_bits(), u32::MAX);
assert!(make(2.0, [0.0; 8], state).is_err());
assert!(make(f32::NAN, [0.0; 8], state).is_err());
assert!(make(1.0, [f32::INFINITY; 8], state).is_err());
assert!(
make(
1.0,
[0.0; 8],
AnalyticInstanceState {
rotation: f32::NAN,
..state
}
)
.is_err()
);
}
}