use crate::math::{Fix128, Vec3Fix};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Clone, Debug)]
pub struct FluidSnapshot {
pub particle_count: u32,
pub positions: Vec<u8>,
pub velocities: Vec<u8>,
pub checksum: u64,
pub frame: u64,
}
impl FluidSnapshot {
#[must_use]
pub fn capture(positions: &[Vec3Fix], velocities: &[Vec3Fix], frame: u64) -> Self {
let n = positions.len();
let pos_data = serialize_vec3_array(positions);
let vel_data = serialize_vec3_array(velocities);
let checksum = compute_checksum(&pos_data, &vel_data);
Self {
particle_count: n as u32,
positions: pos_data,
velocities: vel_data,
checksum,
frame,
}
}
#[must_use]
pub fn restore(&self) -> Option<(Vec<Vec3Fix>, Vec<Vec3Fix>)> {
let n = self.particle_count as usize;
let positions = deserialize_vec3_array(&self.positions, n)?;
let velocities = deserialize_vec3_array(&self.velocities, n)?;
Some((positions, velocities))
}
#[must_use]
pub fn verify(&self, positions: &[Vec3Fix], velocities: &[Vec3Fix]) -> bool {
let pos_data = serialize_vec3_array(positions);
let vel_data = serialize_vec3_array(velocities);
let checksum = compute_checksum(&pos_data, &vel_data);
checksum == self.checksum
}
#[must_use]
pub fn size_bytes(&self) -> usize {
4 + self.positions.len() + self.velocities.len() + 8 + 8
}
}
#[derive(Clone, Debug)]
pub struct FluidDelta {
pub frame: u64,
pub base_frame: u64,
pub changed_indices: Vec<u32>,
pub positions: Vec<Vec3Fix>,
pub velocities: Vec<Vec3Fix>,
pub checksum: u64,
}
impl FluidDelta {
#[must_use]
pub fn compute(
old_positions: &[Vec3Fix],
old_velocities: &[Vec3Fix],
new_positions: &[Vec3Fix],
new_velocities: &[Vec3Fix],
threshold: Fix128,
base_frame: u64,
new_frame: u64,
) -> Self {
let mut changed_indices = Vec::new();
let mut positions = Vec::new();
let mut velocities = Vec::new();
let threshold_sq = threshold * threshold;
for i in 0..new_positions.len().min(old_positions.len()) {
let pos_diff = (new_positions[i] - old_positions[i]).length_squared();
let vel_diff = (new_velocities[i] - old_velocities[i]).length_squared();
if pos_diff > threshold_sq || vel_diff > threshold_sq {
changed_indices.push(i as u32);
positions.push(new_positions[i]);
velocities.push(new_velocities[i]);
}
}
let pos_data = serialize_vec3_array(new_positions);
let vel_data = serialize_vec3_array(new_velocities);
let checksum = compute_checksum(&pos_data, &vel_data);
Self {
frame: new_frame,
base_frame,
changed_indices,
positions,
velocities,
checksum,
}
}
pub fn apply(&self, base_positions: &mut [Vec3Fix], base_velocities: &mut [Vec3Fix]) {
for (idx, &particle_idx) in self.changed_indices.iter().enumerate() {
let i = particle_idx as usize;
if i < base_positions.len() {
base_positions[i] = self.positions[idx];
base_velocities[i] = self.velocities[idx];
}
}
}
#[must_use]
pub fn changed_count(&self) -> usize {
self.changed_indices.len()
}
#[must_use]
pub fn compression_ratio(&self, total_particles: usize) -> f32 {
if total_particles == 0 {
return 1.0;
}
self.changed_indices.len() as f32 / total_particles as f32
}
}
fn serialize_vec3_array(vecs: &[Vec3Fix]) -> Vec<u8> {
let mut data = Vec::with_capacity(vecs.len() * 48);
for v in vecs {
for component in &[v.x, v.y, v.z] {
data.extend_from_slice(&component.hi.to_le_bytes());
data.extend_from_slice(&component.lo.to_le_bytes());
}
}
data
}
fn deserialize_vec3_array(data: &[u8], count: usize) -> Option<Vec<Vec3Fix>> {
let expected = count * 48;
if data.len() < expected {
return None;
}
let mut vecs = Vec::with_capacity(count);
let mut offset = 0;
for _ in 0..count {
let mut components = [Fix128::ZERO; 3];
for c in &mut components {
if offset + 16 > data.len() {
return None;
}
let hi = i64::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
]);
offset += 8;
let lo = u64::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
]);
offset += 8;
*c = Fix128 { hi, lo };
}
vecs.push(Vec3Fix::new(components[0], components[1], components[2]));
}
Some(vecs)
}
fn compute_checksum(pos_data: &[u8], vel_data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325;
for &byte in pos_data.iter().chain(vel_data.iter()) {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_snapshot_roundtrip() {
let positions = vec![Vec3Fix::from_int(1, 2, 3), Vec3Fix::from_int(4, 5, 6)];
let velocities = vec![
Vec3Fix::from_f32(0.1, 0.2, 0.3),
Vec3Fix::from_f32(0.4, 0.5, 0.6),
];
let snapshot = FluidSnapshot::capture(&positions, &velocities, 42);
let (restored_pos, restored_vel) = snapshot.restore().unwrap();
assert_eq!(restored_pos.len(), 2);
assert_eq!(restored_vel.len(), 2);
assert_eq!(restored_pos[0].x.hi, 1);
assert_eq!(restored_pos[1].y.hi, 5);
}
#[test]
fn test_snapshot_verify() {
let positions = vec![Vec3Fix::from_int(1, 2, 3)];
let velocities = vec![Vec3Fix::ZERO];
let snapshot = FluidSnapshot::capture(&positions, &velocities, 0);
assert!(snapshot.verify(&positions, &velocities));
let modified = vec![Vec3Fix::from_int(99, 99, 99)];
assert!(!snapshot.verify(&modified, &velocities));
}
#[test]
fn test_delta_compression() {
let old_pos = vec![
Vec3Fix::from_int(0, 0, 0),
Vec3Fix::from_int(1, 0, 0),
Vec3Fix::from_int(2, 0, 0),
];
let old_vel = vec![Vec3Fix::ZERO; 3];
let mut new_pos = old_pos.clone();
new_pos[1] = Vec3Fix::from_int(1, 5, 0);
let new_vel = old_vel.clone();
let delta = FluidDelta::compute(
&old_pos,
&old_vel,
&new_pos,
&new_vel,
Fix128::from_ratio(1, 10),
0,
1,
);
assert_eq!(delta.changed_count(), 1, "Only one particle changed");
assert!(
delta.compression_ratio(3) < 0.5,
"Should be well compressed"
);
}
#[test]
fn test_delta_apply() {
let old_pos = vec![Vec3Fix::ZERO; 3];
let old_vel = vec![Vec3Fix::ZERO; 3];
let new_pos = vec![Vec3Fix::ZERO, Vec3Fix::from_int(5, 5, 5), Vec3Fix::ZERO];
let new_vel = old_vel.clone();
let delta = FluidDelta::compute(
&old_pos,
&old_vel,
&new_pos,
&new_vel,
Fix128::from_ratio(1, 10),
0,
1,
);
let mut base_pos = old_pos;
let mut base_vel = old_vel;
delta.apply(&mut base_pos, &mut base_vel);
assert_eq!(base_pos[1].x.hi, 5, "Delta should update changed particle");
assert!(base_pos[0].x.is_zero(), "Unchanged particle should remain");
}
}