1use crate::math::{Fix128, Vec3Fix};
17
18#[cfg(not(feature = "std"))]
19use alloc::vec::Vec;
20
21#[derive(Clone, Debug)]
27pub struct FluidSnapshot {
28 pub particle_count: u32,
30 pub positions: Vec<u8>,
32 pub velocities: Vec<u8>,
34 pub checksum: u64,
38 pub frame: u64,
40}
41
42impl FluidSnapshot {
43 #[must_use]
58 pub fn capture(positions: &[Vec3Fix], velocities: &[Vec3Fix], frame: u64) -> Self {
59 assert_eq!(positions.len(), velocities.len(), "length mismatch");
60 let n = positions.len();
61 let pos_data = serialize_vec3_array(positions);
62 let vel_data = serialize_vec3_array(velocities);
63
64 let checksum = compute_checksum(&pos_data, &vel_data);
65
66 Self {
67 particle_count: n as u32,
68 positions: pos_data,
69 velocities: vel_data,
70 checksum,
71 frame,
72 }
73 }
74
75 #[must_use]
77 pub fn restore(&self) -> Option<(Vec<Vec3Fix>, Vec<Vec3Fix>)> {
78 let n = self.particle_count as usize;
79 let positions = deserialize_vec3_array(&self.positions, n)?;
80 let velocities = deserialize_vec3_array(&self.velocities, n)?;
81 Some((positions, velocities))
82 }
83
84 #[must_use]
86 pub fn verify(&self, positions: &[Vec3Fix], velocities: &[Vec3Fix]) -> bool {
87 let pos_data = serialize_vec3_array(positions);
88 let vel_data = serialize_vec3_array(velocities);
89 let checksum = compute_checksum(&pos_data, &vel_data);
90 checksum == self.checksum
91 }
92
93 #[must_use]
95 pub fn size_bytes(&self) -> usize {
96 4 + self.positions.len() + self.velocities.len() + 8 + 8
97 }
98}
99
100#[derive(Clone, Debug)]
109pub struct FluidDelta {
110 pub frame: u64,
112 pub base_frame: u64,
114 pub changed_indices: Vec<u32>,
116 pub positions: Vec<Vec3Fix>,
118 pub velocities: Vec<Vec3Fix>,
120 pub checksum: u64,
122}
123
124impl FluidDelta {
125 #[must_use]
127 pub fn compute(
128 old_positions: &[Vec3Fix],
129 old_velocities: &[Vec3Fix],
130 new_positions: &[Vec3Fix],
131 new_velocities: &[Vec3Fix],
132 threshold: Fix128,
133 base_frame: u64,
134 new_frame: u64,
135 ) -> Self {
136 let mut changed_indices = Vec::new();
137 let mut positions = Vec::new();
138 let mut velocities = Vec::new();
139
140 let exceeds =
147 |d: Vec3Fix| d.x.abs() > threshold || d.y.abs() > threshold || d.z.abs() > threshold;
148
149 let n = new_positions
154 .len()
155 .min(old_positions.len())
156 .min(new_velocities.len())
157 .min(old_velocities.len());
158 for i in 0..n {
159 let pos_diff = new_positions[i] - old_positions[i];
160 let vel_diff = new_velocities[i] - old_velocities[i];
161
162 if exceeds(pos_diff) || exceeds(vel_diff) {
163 changed_indices.push(i as u32);
164 positions.push(new_positions[i]);
165 velocities.push(new_velocities[i]);
166 }
167 }
168
169 let pos_data = serialize_vec3_array(new_positions);
170 let vel_data = serialize_vec3_array(new_velocities);
171 let checksum = compute_checksum(&pos_data, &vel_data);
172
173 Self {
174 frame: new_frame,
175 base_frame,
176 changed_indices,
177 positions,
178 velocities,
179 checksum,
180 }
181 }
182
183 pub fn apply(&self, base_positions: &mut [Vec3Fix], base_velocities: &mut [Vec3Fix]) {
191 for (idx, &particle_idx) in self.changed_indices.iter().enumerate() {
192 let i = particle_idx as usize;
193 if i < base_positions.len() && i < base_velocities.len() {
194 base_positions[i] = self.positions[idx];
195 base_velocities[i] = self.velocities[idx];
196 }
197 }
198 }
199
200 #[must_use]
202 pub fn changed_count(&self) -> usize {
203 self.changed_indices.len()
204 }
205
206 #[must_use]
208 pub fn compression_ratio(&self, total_particles: usize) -> f32 {
209 if total_particles == 0 {
210 return 1.0;
211 }
212 self.changed_indices.len() as f32 / total_particles as f32
213 }
214}
215
216fn serialize_vec3_array(vecs: &[Vec3Fix]) -> Vec<u8> {
221 let mut data = Vec::with_capacity(vecs.len() * 48); for v in vecs {
224 for component in &[v.x, v.y, v.z] {
225 data.extend_from_slice(&component.hi.to_le_bytes());
226 data.extend_from_slice(&component.lo.to_le_bytes());
227 }
228 }
229
230 data
231}
232
233fn deserialize_vec3_array(data: &[u8], count: usize) -> Option<Vec<Vec3Fix>> {
234 let expected = count * 48;
235 if data.len() < expected {
236 return None;
237 }
238
239 let mut vecs = Vec::with_capacity(count);
240 let mut offset = 0;
241
242 for _ in 0..count {
243 let mut components = [Fix128::ZERO; 3];
244 for c in &mut components {
245 if offset + 16 > data.len() {
246 return None;
247 }
248 let hi = i64::from_le_bytes([
249 data[offset],
250 data[offset + 1],
251 data[offset + 2],
252 data[offset + 3],
253 data[offset + 4],
254 data[offset + 5],
255 data[offset + 6],
256 data[offset + 7],
257 ]);
258 offset += 8;
259 let lo = u64::from_le_bytes([
260 data[offset],
261 data[offset + 1],
262 data[offset + 2],
263 data[offset + 3],
264 data[offset + 4],
265 data[offset + 5],
266 data[offset + 6],
267 data[offset + 7],
268 ]);
269 offset += 8;
270 *c = Fix128 { hi, lo };
271 }
272 vecs.push(Vec3Fix::new(components[0], components[1], components[2]));
273 }
274
275 Some(vecs)
276}
277
278fn compute_checksum(pos_data: &[u8], vel_data: &[u8]) -> u64 {
283 let pos_len = (pos_data.len() as u64).to_le_bytes();
284 let vel_len = (vel_data.len() as u64).to_le_bytes();
285 let mut hash: u64 = 0xcbf29ce484222325;
286 for &byte in pos_len
287 .iter()
288 .chain(pos_data)
289 .chain(vel_len.iter())
290 .chain(vel_data)
291 {
292 hash ^= byte as u64;
293 hash = hash.wrapping_mul(0x100000001b3);
294 }
295 hash
296}
297
298#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_snapshot_roundtrip() {
308 let positions = vec![Vec3Fix::from_int(1, 2, 3), Vec3Fix::from_int(4, 5, 6)];
309 let velocities = vec![
310 Vec3Fix::from_f32(0.1, 0.2, 0.3),
311 Vec3Fix::from_f32(0.4, 0.5, 0.6),
312 ];
313
314 let snapshot = FluidSnapshot::capture(&positions, &velocities, 42);
315 let (restored_pos, restored_vel) = snapshot.restore().unwrap();
316
317 assert_eq!(restored_pos.len(), 2);
318 assert_eq!(restored_vel.len(), 2);
319 assert_eq!(restored_pos[0].x.hi, 1);
320 assert_eq!(restored_pos[1].y.hi, 5);
321 }
322
323 #[test]
324 fn test_snapshot_verify() {
325 let positions = vec![Vec3Fix::from_int(1, 2, 3)];
326 let velocities = vec![Vec3Fix::ZERO];
327
328 let snapshot = FluidSnapshot::capture(&positions, &velocities, 0);
329 assert!(snapshot.verify(&positions, &velocities));
330
331 let modified = vec![Vec3Fix::from_int(99, 99, 99)];
333 assert!(!snapshot.verify(&modified, &velocities));
334 }
335
336 #[test]
337 fn test_delta_compression() {
338 let old_pos = vec![
339 Vec3Fix::from_int(0, 0, 0),
340 Vec3Fix::from_int(1, 0, 0),
341 Vec3Fix::from_int(2, 0, 0),
342 ];
343 let old_vel = vec![Vec3Fix::ZERO; 3];
344
345 let mut new_pos = old_pos.clone();
347 new_pos[1] = Vec3Fix::from_int(1, 5, 0);
348 let new_vel = old_vel.clone();
349
350 let delta = FluidDelta::compute(
351 &old_pos,
352 &old_vel,
353 &new_pos,
354 &new_vel,
355 Fix128::from_ratio(1, 10),
356 0,
357 1,
358 );
359
360 assert_eq!(delta.changed_count(), 1, "Only one particle changed");
361 assert!(
362 delta.compression_ratio(3) < 0.5,
363 "Should be well compressed"
364 );
365 }
366
367 #[test]
368 fn test_delta_apply() {
369 let old_pos = vec![Vec3Fix::ZERO; 3];
370 let old_vel = vec![Vec3Fix::ZERO; 3];
371
372 let new_pos = vec![Vec3Fix::ZERO, Vec3Fix::from_int(5, 5, 5), Vec3Fix::ZERO];
373 let new_vel = old_vel.clone();
374
375 let delta = FluidDelta::compute(
376 &old_pos,
377 &old_vel,
378 &new_pos,
379 &new_vel,
380 Fix128::from_ratio(1, 10),
381 0,
382 1,
383 );
384
385 let mut base_pos = old_pos;
386 let mut base_vel = old_vel;
387 delta.apply(&mut base_pos, &mut base_vel);
388
389 assert_eq!(base_pos[1].x.hi, 5, "Delta should update changed particle");
390 assert!(base_pos[0].x.is_zero(), "Unchanged particle should remain");
391 }
392}