Skip to main content

oxihuman_morph/
morph_delta_stream.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Streaming morph delta application for incremental updates.
5
6#[allow(dead_code)]
7pub struct DeltaChunk {
8    pub target_name: String,
9    pub weight: f32,
10    pub start_vertex: usize,
11    pub deltas: Vec<[f32; 3]>,
12}
13
14#[allow(dead_code)]
15pub struct DeltaStream {
16    pub chunks: Vec<DeltaChunk>,
17    pub total_vertices: usize,
18    pub dirty: bool,
19}
20
21#[allow(dead_code)]
22pub struct StreamConfig {
23    pub chunk_size: usize,
24    pub threshold: f32,
25    pub accumulate: bool,
26}
27
28#[allow(dead_code)]
29pub fn default_stream_config() -> StreamConfig {
30    StreamConfig {
31        chunk_size: 256,
32        threshold: 1e-6,
33        accumulate: true,
34    }
35}
36
37#[allow(dead_code)]
38pub fn new_delta_stream(vertex_count: usize) -> DeltaStream {
39    DeltaStream {
40        chunks: Vec::new(),
41        total_vertices: vertex_count,
42        dirty: false,
43    }
44}
45
46#[allow(dead_code)]
47pub fn push_chunk(stream: &mut DeltaStream, chunk: DeltaChunk) {
48    stream.chunks.push(chunk);
49    stream.dirty = true;
50}
51
52#[allow(dead_code)]
53pub fn apply_stream(stream: &DeltaStream, positions: &mut [[f32; 3]]) {
54    for chunk in &stream.chunks {
55        apply_chunk(chunk, positions);
56    }
57}
58
59#[allow(dead_code)]
60pub fn apply_chunk(chunk: &DeltaChunk, positions: &mut [[f32; 3]]) {
61    for (i, delta) in chunk.deltas.iter().enumerate() {
62        let vi = chunk.start_vertex + i;
63        if vi < positions.len() {
64            positions[vi][0] += delta[0] * chunk.weight;
65            positions[vi][1] += delta[1] * chunk.weight;
66            positions[vi][2] += delta[2] * chunk.weight;
67        }
68    }
69}
70
71#[allow(dead_code)]
72pub fn split_deltas_into_chunks(
73    target: &str,
74    weight: f32,
75    deltas: &[(usize, [f32; 3])],
76    chunk_size: usize,
77) -> Vec<DeltaChunk> {
78    if deltas.is_empty() {
79        return Vec::new();
80    }
81    let mut result = Vec::new();
82    for window in deltas.chunks(chunk_size) {
83        let start_vertex = window[0].0;
84        let chunk_deltas: Vec<[f32; 3]> = window.iter().map(|(_, d)| *d).collect();
85        result.push(DeltaChunk {
86            target_name: target.to_string(),
87            weight,
88            start_vertex,
89            deltas: chunk_deltas,
90        });
91    }
92    result
93}
94
95#[allow(dead_code)]
96pub fn merge_chunks(chunks: &[DeltaChunk]) -> DeltaChunk {
97    if chunks.is_empty() {
98        return DeltaChunk {
99            target_name: String::new(),
100            weight: 0.0,
101            start_vertex: 0,
102            deltas: Vec::new(),
103        };
104    }
105    let target_name = chunks[0].target_name.clone();
106    let weight = chunks[0].weight;
107    let start_vertex = chunks.iter().map(|c| c.start_vertex).min().unwrap_or(0);
108    let max_end = chunks
109        .iter()
110        .map(|c| c.start_vertex + c.deltas.len())
111        .max()
112        .unwrap_or(0);
113    let capacity = max_end.saturating_sub(start_vertex);
114    let mut merged: Vec<[f32; 3]> = vec![[0.0, 0.0, 0.0]; capacity];
115    for chunk in chunks {
116        for (i, delta) in chunk.deltas.iter().enumerate() {
117            let vi = chunk.start_vertex + i;
118            if vi >= start_vertex && vi - start_vertex < merged.len() {
119                let idx = vi - start_vertex;
120                merged[idx][0] += delta[0];
121                merged[idx][1] += delta[1];
122                merged[idx][2] += delta[2];
123            }
124        }
125    }
126    DeltaChunk {
127        target_name,
128        weight,
129        start_vertex,
130        deltas: merged,
131    }
132}
133
134#[allow(dead_code)]
135pub fn stream_delta_count(stream: &DeltaStream) -> usize {
136    stream.chunks.iter().map(|c| c.deltas.len()).sum()
137}
138
139#[allow(dead_code)]
140pub fn clear_stream(stream: &mut DeltaStream) {
141    stream.chunks.clear();
142    stream.dirty = false;
143}
144
145#[allow(dead_code)]
146pub fn stream_chunk_count(stream: &DeltaStream) -> usize {
147    stream.chunks.len()
148}
149
150#[allow(dead_code)]
151pub fn filter_stream_threshold(stream: &mut DeltaStream, threshold: f32) {
152    for chunk in &mut stream.chunks {
153        chunk.deltas.retain(|d| {
154            let mag = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
155            mag >= threshold
156        });
157    }
158    stream.chunks.retain(|c| !c.deltas.is_empty());
159}
160
161#[allow(dead_code)]
162pub fn stream_memory_bytes(stream: &DeltaStream) -> usize {
163    let chunk_overhead = stream.chunks.len()
164        * (std::mem::size_of::<usize>() * 2
165            + std::mem::size_of::<f32>()
166            + std::mem::size_of::<usize>());
167    let delta_bytes: usize = stream
168        .chunks
169        .iter()
170        .map(|c| c.deltas.len() * std::mem::size_of::<[f32; 3]>())
171        .sum();
172    chunk_overhead + delta_bytes
173}
174
175#[allow(dead_code)]
176pub fn stream_to_flat_deltas(stream: &DeltaStream) -> Vec<(usize, [f32; 3])> {
177    let mut per_vertex: std::collections::HashMap<usize, [f32; 3]> =
178        std::collections::HashMap::new();
179    for chunk in &stream.chunks {
180        for (i, delta) in chunk.deltas.iter().enumerate() {
181            let vi = chunk.start_vertex + i;
182            let entry = per_vertex.entry(vi).or_insert([0.0, 0.0, 0.0]);
183            entry[0] += delta[0] * chunk.weight;
184            entry[1] += delta[1] * chunk.weight;
185            entry[2] += delta[2] * chunk.weight;
186        }
187    }
188    let mut result: Vec<(usize, [f32; 3])> = per_vertex.into_iter().collect();
189    result.sort_by_key(|(vi, _)| *vi);
190    result
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_default_stream_config() {
199        let cfg = default_stream_config();
200        assert_eq!(cfg.chunk_size, 256);
201        assert!(cfg.threshold < 1e-5);
202        assert!(cfg.accumulate);
203    }
204
205    #[test]
206    fn test_new_delta_stream() {
207        let stream = new_delta_stream(1000);
208        assert_eq!(stream.total_vertices, 1000);
209        assert_eq!(stream.chunks.len(), 0);
210        assert!(!stream.dirty);
211    }
212
213    #[test]
214    fn test_push_chunk() {
215        let mut stream = new_delta_stream(100);
216        let chunk = DeltaChunk {
217            target_name: "smile".to_string(),
218            weight: 1.0,
219            start_vertex: 0,
220            deltas: vec![[1.0, 0.0, 0.0]],
221        };
222        push_chunk(&mut stream, chunk);
223        assert_eq!(stream_chunk_count(&stream), 1);
224        assert!(stream.dirty);
225    }
226
227    #[test]
228    fn test_apply_stream() {
229        let mut stream = new_delta_stream(4);
230        let chunk = DeltaChunk {
231            target_name: "test".to_string(),
232            weight: 0.5,
233            start_vertex: 0,
234            deltas: vec![[2.0, 0.0, 0.0], [0.0, 2.0, 0.0]],
235        };
236        push_chunk(&mut stream, chunk);
237        let mut positions = [[0.0f32; 3]; 4];
238        apply_stream(&stream, &mut positions);
239        assert!((positions[0][0] - 1.0).abs() < 1e-5);
240        assert!((positions[1][1] - 1.0).abs() < 1e-5);
241    }
242
243    #[test]
244    fn test_apply_chunk() {
245        let chunk = DeltaChunk {
246            target_name: "brow".to_string(),
247            weight: 2.0,
248            start_vertex: 1,
249            deltas: vec![[1.0, 0.5, 0.0]],
250        };
251        let mut positions = [[0.0f32; 3]; 4];
252        apply_chunk(&chunk, &mut positions);
253        assert!((positions[1][0] - 2.0).abs() < 1e-5);
254        assert!((positions[1][1] - 1.0).abs() < 1e-5);
255    }
256
257    #[test]
258    fn test_split_deltas_into_chunks() {
259        let deltas: Vec<(usize, [f32; 3])> = (0..10).map(|i| (i, [1.0, 0.0, 0.0])).collect();
260        let chunks = split_deltas_into_chunks("target", 1.0, &deltas, 4);
261        assert_eq!(chunks.len(), 3);
262        assert_eq!(chunks[0].deltas.len(), 4);
263        assert_eq!(chunks[1].deltas.len(), 4);
264        assert_eq!(chunks[2].deltas.len(), 2);
265    }
266
267    #[test]
268    fn test_split_empty_deltas() {
269        let chunks = split_deltas_into_chunks("target", 1.0, &[], 4);
270        assert!(chunks.is_empty());
271    }
272
273    #[test]
274    fn test_stream_chunk_count() {
275        let mut stream = new_delta_stream(100);
276        assert_eq!(stream_chunk_count(&stream), 0);
277        push_chunk(
278            &mut stream,
279            DeltaChunk {
280                target_name: "t".to_string(),
281                weight: 1.0,
282                start_vertex: 0,
283                deltas: vec![[0.0, 0.0, 0.0]],
284            },
285        );
286        assert_eq!(stream_chunk_count(&stream), 1);
287    }
288
289    #[test]
290    fn test_stream_delta_count() {
291        let mut stream = new_delta_stream(100);
292        push_chunk(
293            &mut stream,
294            DeltaChunk {
295                target_name: "a".to_string(),
296                weight: 1.0,
297                start_vertex: 0,
298                deltas: vec![[1.0, 0.0, 0.0]; 5],
299            },
300        );
301        push_chunk(
302            &mut stream,
303            DeltaChunk {
304                target_name: "b".to_string(),
305                weight: 1.0,
306                start_vertex: 5,
307                deltas: vec![[0.0, 1.0, 0.0]; 3],
308            },
309        );
310        assert_eq!(stream_delta_count(&stream), 8);
311    }
312
313    #[test]
314    fn test_clear_stream() {
315        let mut stream = new_delta_stream(100);
316        push_chunk(
317            &mut stream,
318            DeltaChunk {
319                target_name: "t".to_string(),
320                weight: 1.0,
321                start_vertex: 0,
322                deltas: vec![[1.0, 0.0, 0.0]],
323            },
324        );
325        clear_stream(&mut stream);
326        assert_eq!(stream_chunk_count(&stream), 0);
327        assert!(!stream.dirty);
328    }
329
330    #[test]
331    fn test_filter_stream_threshold() {
332        let mut stream = new_delta_stream(10);
333        push_chunk(
334            &mut stream,
335            DeltaChunk {
336                target_name: "t".to_string(),
337                weight: 1.0,
338                start_vertex: 0,
339                deltas: vec![[0.0001, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0001, 0.0]],
340            },
341        );
342        filter_stream_threshold(&mut stream, 0.01);
343        assert_eq!(stream_delta_count(&stream), 1);
344    }
345
346    #[test]
347    fn test_stream_to_flat_deltas() {
348        let mut stream = new_delta_stream(10);
349        push_chunk(
350            &mut stream,
351            DeltaChunk {
352                target_name: "a".to_string(),
353                weight: 1.0,
354                start_vertex: 0,
355                deltas: vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
356            },
357        );
358        push_chunk(
359            &mut stream,
360            DeltaChunk {
361                target_name: "b".to_string(),
362                weight: 2.0,
363                start_vertex: 0,
364                deltas: vec![[0.5, 0.0, 0.0]],
365            },
366        );
367        let flat = stream_to_flat_deltas(&stream);
368        assert!(!flat.is_empty());
369        let v0 = flat
370            .iter()
371            .find(|(vi, _)| *vi == 0)
372            .expect("should succeed");
373        assert!((v0.1[0] - 2.0).abs() < 1e-5);
374    }
375
376    #[test]
377    fn test_merge_chunks() {
378        let chunks = vec![
379            DeltaChunk {
380                target_name: "smile".to_string(),
381                weight: 1.0,
382                start_vertex: 0,
383                deltas: vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
384            },
385            DeltaChunk {
386                target_name: "smile".to_string(),
387                weight: 1.0,
388                start_vertex: 2,
389                deltas: vec![[0.0, 0.0, 1.0]],
390            },
391        ];
392        let merged = merge_chunks(&chunks);
393        assert_eq!(merged.start_vertex, 0);
394        assert_eq!(merged.deltas.len(), 3);
395    }
396
397    #[test]
398    fn test_stream_memory_bytes() {
399        let mut stream = new_delta_stream(100);
400        push_chunk(
401            &mut stream,
402            DeltaChunk {
403                target_name: "t".to_string(),
404                weight: 1.0,
405                start_vertex: 0,
406                deltas: vec![[1.0, 0.0, 0.0]; 10],
407            },
408        );
409        let bytes = stream_memory_bytes(&stream);
410        assert!(bytes > 0);
411    }
412
413    #[test]
414    fn test_apply_stream_out_of_bounds() {
415        let mut stream = new_delta_stream(2);
416        push_chunk(
417            &mut stream,
418            DeltaChunk {
419                target_name: "t".to_string(),
420                weight: 1.0,
421                start_vertex: 100,
422                deltas: vec![[1.0, 0.0, 0.0]],
423            },
424        );
425        let mut positions = [[0.0f32; 3]; 2];
426        // should not panic
427        apply_stream(&stream, &mut positions);
428        assert!((positions[0][0]).abs() < 1e-5);
429    }
430}