Skip to main content

laser_dac/
frame_adapter.rs

1//! Frame adapter for converting point buffers to continuous streams.
2//!
3//! This module provides [`FrameAdapter`] which converts a point buffer (frame)
4//! into a continuous stream of points for the DAC. The adapter cycles through
5//! the frame's points, producing chunks on demand.
6//!
7//! # Update Semantics
8//!
9//! - **Latest-wins**: `update()` sets the pending frame. Multiple calls before
10//!   a swap keep only the most recent.
11//! - **Immediate swap on frame end**: When the current frame completes (all
12//!   points output), any pending frame becomes current immediately—even
13//!   mid-chunk. This ensures clean frame-to-frame transitions.
14//!
15//! The adapter does not insert blanking on frame swaps. If the new frame's first
16//! point is far from the previous frame's last point, content should include
17//! lead-in blanking to avoid visible travel lines.
18//!
19//! # Example
20//!
21//! ```ignore
22//! use laser_dac::{ChunkRequest, ChunkResult, LaserPoint, Frame, FrameAdapter};
23//!
24//! let mut adapter = FrameAdapter::new();
25//! adapter.update(Frame::new(vec![
26//!     LaserPoint::new(0.5, 0.0, 65535, 0, 0, 65535),
27//!     LaserPoint::new(0.0, 0.5, 0, 65535, 0, 65535),
28//! ]));
29//! let shared = adapter.shared(); // Thread-safe handle
30//!
31//! stream.run(
32//!     |req: &ChunkRequest, buffer: &mut [LaserPoint]| {
33//!         shared.fill_chunk(req, buffer)
34//!     },
35//!     |err| eprintln!("Error: {}", err),
36//! )?;
37//! ```
38//!
39//! For time-varying animation, use the streaming API directly with a point
40//! generator (see the `manual` or `callback` examples with `orbiting-circle`).
41
42use std::sync::{Arc, Mutex};
43
44use crate::types::{ChunkRequest, ChunkResult, LaserPoint};
45
46/// A point buffer to be cycled by the adapter.
47#[derive(Clone, Debug)]
48pub struct Frame {
49    pub points: Vec<LaserPoint>,
50}
51
52impl Frame {
53    /// Creates a new frame from a vector of points.
54    pub fn new(points: Vec<LaserPoint>) -> Self {
55        Self { points }
56    }
57
58    /// Creates an empty frame (outputs blanked points at last position).
59    pub fn empty() -> Self {
60        Self { points: Vec::new() }
61    }
62}
63
64impl From<Vec<LaserPoint>> for Frame {
65    fn from(points: Vec<LaserPoint>) -> Self {
66        Self::new(points)
67    }
68}
69
70/// Converts a point buffer (frame) into a continuous stream.
71///
72/// The adapter cycles through the frame's points, filling buffers via
73/// the `fill_chunk()` method for use with `Stream::run()`.
74///
75/// # Update semantics
76///
77/// - `update()` sets the pending frame (latest-wins if called multiple times)
78/// - When the current frame ends, any pending frame becomes current immediately
79///   (even mid-chunk), ensuring clean frame-to-frame transitions
80///
81/// # Example
82///
83/// ```ignore
84/// use laser_dac::{Frame, FrameAdapter, LaserPoint};
85///
86/// let mut adapter = FrameAdapter::new();
87/// adapter.update(Frame::new(vec![
88///     LaserPoint::new(0.5, 0.0, 65535, 0, 0, 65535),
89/// ]));
90/// let shared = adapter.shared();
91///
92/// stream.run(
93///     |req, buffer| shared.fill_chunk(req, buffer),
94///     |err| eprintln!("Error: {}", err),
95/// )?;
96/// ```
97pub struct FrameAdapter {
98    current: Frame,
99    pending: Option<Frame>,
100    point_index: usize,
101    last_position: (f32, f32),
102}
103
104impl FrameAdapter {
105    /// Creates a new adapter with an empty frame.
106    ///
107    /// Call `update()` to set the initial frame before streaming.
108    pub fn new() -> Self {
109        Self {
110            current: Frame::empty(),
111            pending: None,
112            point_index: 0,
113            last_position: (0.0, 0.0),
114        }
115    }
116
117    /// Sets the pending frame.
118    ///
119    /// The frame becomes current when the current frame ends (all points
120    /// output). If called multiple times before a swap, only the most recent
121    /// frame is kept (latest-wins).
122    pub fn update(&mut self, frame: Frame) {
123        self.pending = Some(frame);
124    }
125
126    /// Fill the provided buffer with points from the current frame.
127    ///
128    /// This is the new zero-allocation API that fills a library-owned buffer
129    /// instead of allocating a new Vec on each call.
130    ///
131    /// Cycles through the current frame. When the frame ends and a pending
132    /// frame is available, switches immediately (even mid-chunk).
133    ///
134    /// # Returns
135    ///
136    /// - `ChunkResult::Filled(n)` - Wrote `n` points (always `req.target_points`)
137    /// - `ChunkResult::Starved` - Never returned (adapter always has points to output)
138    /// - `ChunkResult::End` - Never returned (adapter cycles indefinitely)
139    pub fn fill_chunk(&mut self, req: &ChunkRequest, buffer: &mut [LaserPoint]) -> ChunkResult {
140        let n_points = req.target_points.min(buffer.len());
141        self.fill_points(buffer, n_points);
142        ChunkResult::Filled(n_points)
143    }
144
145    /// Fill buffer with n_points from the frame (zero-allocation).
146    #[allow(clippy::needless_range_loop)] // Index used with continue
147    fn fill_points(&mut self, buffer: &mut [LaserPoint], n_points: usize) {
148        for i in 0..n_points {
149            // Handle empty frame: try to swap, else output blanked
150            if self.current.points.is_empty() {
151                if let Some(pending) = self.pending.take() {
152                    self.current = pending;
153                    self.point_index = 0;
154                }
155
156                if self.current.points.is_empty() {
157                    let (x, y) = self.last_position;
158                    buffer[i] = LaserPoint::blanked(x, y);
159                    continue;
160                }
161            }
162
163            let point = self.current.points[self.point_index];
164            buffer[i] = point;
165            self.last_position = (point.x, point.y);
166
167            self.point_index += 1;
168            if self.point_index >= self.current.points.len() {
169                self.point_index = 0;
170                // Immediately swap to pending frame if available
171                if let Some(pending) = self.pending.take() {
172                    self.current = pending;
173                    self.point_index = 0;
174                }
175            }
176        }
177    }
178
179    /// Returns a thread-safe handle for updating frames from another thread.
180    pub fn shared(self) -> SharedFrameAdapter {
181        SharedFrameAdapter {
182            inner: Arc::new(Mutex::new(self)),
183        }
184    }
185}
186
187impl Default for FrameAdapter {
188    fn default() -> Self {
189        Self::new()
190    }
191}
192
193/// Thread-safe handle for updating frames from another thread.
194#[derive(Clone)]
195pub struct SharedFrameAdapter {
196    inner: Arc<Mutex<FrameAdapter>>,
197}
198
199impl SharedFrameAdapter {
200    /// Sets the pending frame. Takes effect when the current frame ends.
201    pub fn update(&self, frame: Frame) {
202        let mut adapter = self.inner.lock().unwrap();
203        adapter.update(frame);
204    }
205
206    /// Fill the provided buffer with points from the current frame.
207    ///
208    /// Thread-safe version of `FrameAdapter::fill_chunk()`.
209    /// See that method for full documentation.
210    pub fn fill_chunk(&self, req: &ChunkRequest, buffer: &mut [LaserPoint]) -> ChunkResult {
211        let mut adapter = self.inner.lock().unwrap();
212        adapter.fill_chunk(req, buffer)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::types::StreamInstant;
220    use std::time::Duration;
221
222    fn make_fill_request(target_points: usize) -> ChunkRequest {
223        ChunkRequest {
224            start: StreamInstant(0),
225            pps: 30000,
226            min_points: target_points,
227            target_points,
228            buffered_points: 0,
229            buffered: Duration::ZERO,
230            device_queued_points: None,
231        }
232    }
233
234    #[test]
235    fn test_empty_frame() {
236        let mut adapter = FrameAdapter::new();
237        let req = make_fill_request(100);
238        let mut buffer = vec![LaserPoint::default(); 100];
239
240        let result = adapter.fill_chunk(&req, &mut buffer);
241        assert!(matches!(result, ChunkResult::Filled(100)));
242        assert!(buffer.iter().all(|p| p.intensity == 0));
243    }
244
245    #[test]
246    fn test_frame_cycles() {
247        let mut adapter = FrameAdapter::new();
248        let frame_points: Vec<LaserPoint> = (0..10)
249            .map(|i| LaserPoint::new(i as f32 / 10.0, 0.0, 65535, 0, 0, 65535))
250            .collect();
251        adapter.update(Frame::new(frame_points));
252
253        let req = make_fill_request(25);
254        let mut buffer = vec![LaserPoint::default(); 25];
255
256        let result = adapter.fill_chunk(&req, &mut buffer);
257        assert!(matches!(result, ChunkResult::Filled(25)));
258    }
259
260    #[test]
261    fn test_single_point_swaps_immediately() {
262        let mut adapter = FrameAdapter::new();
263        adapter.update(Frame::new(vec![LaserPoint::new(
264            0.0, 0.0, 65535, 0, 0, 65535,
265        )]));
266
267        let req = make_fill_request(10);
268        let mut buffer = vec![LaserPoint::default(); 10];
269
270        adapter.fill_chunk(&req, &mut buffer);
271        assert_eq!(buffer[0].x, 0.0);
272
273        adapter.update(Frame::new(vec![LaserPoint::new(
274            1.0, 1.0, 0, 65535, 0, 65535,
275        )]));
276
277        // Single-point frame wraps every point; swap happens immediately after
278        // each point, so first point is old frame, rest is new frame
279        adapter.fill_chunk(&req, &mut buffer);
280        assert_eq!(buffer[0].x, 0.0, "First point finishes old frame");
281        assert_eq!(buffer[1].x, 1.0, "Second point starts new frame");
282        assert!(buffer[2..].iter().all(|p| p.x == 1.0), "Rest is new frame");
283    }
284
285    #[test]
286    fn test_swap_waits_for_frame_end() {
287        let mut adapter = FrameAdapter::new();
288        let frame1: Vec<LaserPoint> = (0..100)
289            .map(|i| LaserPoint::new(i as f32 / 100.0, 0.0, 65535, 0, 0, 65535))
290            .collect();
291        adapter.update(Frame::new(frame1));
292
293        let req = make_fill_request(10);
294        let mut buffer = vec![LaserPoint::default(); 10];
295
296        adapter.fill_chunk(&req, &mut buffer);
297        assert_eq!(buffer[0].x, 0.0);
298
299        // Update mid-cycle with different frame
300        let frame2: Vec<LaserPoint> = (0..100)
301            .map(|_| LaserPoint::new(9.0, 9.0, 0, 65535, 0, 65535))
302            .collect();
303        adapter.update(Frame::new(frame2));
304
305        // Should still use frame1 (not finished yet)
306        adapter.fill_chunk(&req, &mut buffer);
307        assert!(
308            (buffer[0].x - 0.1).abs() < 1e-4,
309            "Expected ~0.1, got {}",
310            buffer[0].x
311        );
312
313        // Output remaining 80 points to complete frame1
314        for _ in 0..8 {
315            adapter.fill_chunk(&req, &mut buffer);
316        }
317
318        // Now frame1 finished, uses frame2
319        adapter.fill_chunk(&req, &mut buffer);
320        assert_eq!(buffer[0].x, 9.0);
321    }
322
323    #[test]
324    fn test_mid_chunk_stitching() {
325        // Frame with 95 points, chunk size 10: wrap happens mid-chunk
326        let mut adapter = FrameAdapter::new();
327        let frame1: Vec<LaserPoint> = (0..95)
328            .map(|i| LaserPoint::new(i as f32, 0.0, 65535, 0, 0, 65535))
329            .collect();
330        adapter.update(Frame::new(frame1));
331
332        let req = make_fill_request(10);
333        let mut buffer = vec![LaserPoint::default(); 10];
334
335        // Output 90 points (9 chunks)
336        for _ in 0..9 {
337            adapter.fill_chunk(&req, &mut buffer);
338        }
339
340        // Now at index 90, update with new frame
341        let frame2: Vec<LaserPoint> = (0..95)
342            .map(|_| LaserPoint::new(999.0, 999.0, 0, 65535, 0, 65535))
343            .collect();
344        adapter.update(Frame::new(frame2));
345
346        // Next chunk: points 90-94 from frame1, then points 0-4 from frame2
347        adapter.fill_chunk(&req, &mut buffer);
348
349        // First 5 points are frame1 (90, 91, 92, 93, 94)
350        for (i, p) in buffer[0..5].iter().enumerate() {
351            assert_eq!(p.x, (90 + i) as f32, "Point {} should be from frame1", i);
352        }
353
354        // Last 5 points are frame2 (all 999.0)
355        for (i, p) in buffer[5..10].iter().enumerate() {
356            assert_eq!(p.x, 999.0, "Point {} should be from frame2", i + 5);
357        }
358    }
359
360    #[test]
361    fn test_empty_holds_last_position() {
362        let mut adapter = FrameAdapter::new();
363        adapter.update(Frame::new(vec![LaserPoint::new(
364            0.5, -0.3, 65535, 0, 0, 65535,
365        )]));
366
367        let req = make_fill_request(5);
368        let mut buffer = vec![LaserPoint::default(); 5];
369
370        adapter.fill_chunk(&req, &mut buffer);
371        adapter.update(Frame::empty());
372
373        // First point finishes the single-point frame, then swap to empty
374        adapter.fill_chunk(&req, &mut buffer);
375        assert_eq!(buffer[0].intensity, 65535, "First point finishes old frame");
376        assert!(
377            buffer[1..].iter().all(|p| p.intensity == 0),
378            "Rest is blanked"
379        );
380        // Empty frame holds the last known position
381        assert_eq!(buffer[1].x, 0.5);
382        assert_eq!(buffer[1].y, -0.3);
383    }
384
385    #[test]
386    fn test_integer_index_deterministic() {
387        let mut adapter = FrameAdapter::new();
388        let frame: Vec<LaserPoint> = (0..7)
389            .map(|i| LaserPoint::new(i as f32, 0.0, 65535, 0, 0, 65535))
390            .collect();
391        adapter.update(Frame::new(frame));
392
393        let req = make_fill_request(7);
394        let mut buffer = vec![LaserPoint::default(); 7];
395
396        // No drift over 1000 cycles
397        for cycle in 0..1000 {
398            adapter.fill_chunk(&req, &mut buffer);
399            for (i, p) in buffer.iter().enumerate() {
400                assert_eq!(p.x, i as f32, "Cycle {}: drift detected", cycle);
401            }
402        }
403    }
404
405    #[test]
406    fn test_from_vec() {
407        let points: Vec<LaserPoint> = vec![LaserPoint::new(0.0, 0.0, 65535, 0, 0, 65535)];
408        let frame: Frame = points.into();
409        assert_eq!(frame.points.len(), 1);
410    }
411}