1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4use std::sync::mpsc;
5use std::sync::{Arc, Mutex};
6use std::thread::JoinHandle;
7use std::time::{Duration, Instant};
8
9use crate::backend::BackendKind;
10use crate::config::StreamConfig;
11use crate::device::{DacInfo, OutputModel};
12use crate::error::{Error, Result};
13use crate::reconnect::ReconnectPolicy;
14use crate::stream::{ControlMsg, RunExit, StreamControl};
15
16use super::driver::{self, DriverInputs, SourceOwned};
17use super::engine::PresentationEngine;
18use super::slice_pipeline::SlicePipeline;
19use super::{default_transition, Frame, OutputResetReason, TransitionFn};
20
21pub struct FrameSessionConfig {
27 pub pps: u32,
29 pub transition_fn: TransitionFn,
31 pub startup_blank: std::time::Duration,
33 pub color_delay_points: usize,
39 pub reconnect: Option<crate::config::ReconnectConfig>,
44 pub idle_policy: crate::config::IdlePolicy,
50 pub output_filter: Option<Box<dyn super::OutputFilter>>,
52}
53
54impl FrameSessionConfig {
55 const DEFAULT_COLOR_DELAY: std::time::Duration = std::time::Duration::from_micros(150);
56
57 pub fn new(pps: u32) -> Self {
59 let color_delay_points =
60 (Self::DEFAULT_COLOR_DELAY.as_secs_f64() * pps as f64).ceil() as usize;
61 Self {
62 pps,
63 transition_fn: default_transition(pps),
64 startup_blank: std::time::Duration::from_millis(1),
65 color_delay_points,
66 idle_policy: crate::config::IdlePolicy::default(),
67 reconnect: None,
68 output_filter: None,
69 }
70 }
71
72 pub fn with_transition_fn(mut self, f: TransitionFn) -> Self {
74 self.transition_fn = f;
75 self
76 }
77
78 pub fn with_startup_blank(mut self, duration: std::time::Duration) -> Self {
80 self.startup_blank = duration;
81 self
82 }
83
84 pub fn with_color_delay_points(mut self, n: usize) -> Self {
86 self.color_delay_points = n;
87 self
88 }
89
90 pub fn with_reconnect(mut self, config: crate::config::ReconnectConfig) -> Self {
94 self.reconnect = Some(config);
95 self
96 }
97
98 pub fn with_idle_policy(mut self, policy: crate::config::IdlePolicy) -> Self {
102 self.idle_policy = policy;
103 self
104 }
105
106 pub fn with_output_filter(mut self, filter: Box<dyn super::OutputFilter>) -> Self {
108 self.output_filter = Some(filter);
109 self
110 }
111}
112
113#[derive(Clone)]
119pub struct FrameSessionMetrics {
120 inner: Arc<FrameSessionMetricsInner>,
121}
122
123struct FrameSessionMetricsInner {
124 connected: AtomicBool,
125 origin: Instant,
126 last_loop_activity_nanos: AtomicU64,
127 last_write_success_nanos: AtomicU64,
128}
129
130impl FrameSessionMetrics {
131 pub(crate) fn new(connected: bool) -> Self {
132 let metrics = Self {
133 inner: Arc::new(FrameSessionMetricsInner {
134 connected: AtomicBool::new(connected),
135 origin: Instant::now(),
136 last_loop_activity_nanos: AtomicU64::new(0),
137 last_write_success_nanos: AtomicU64::new(0),
138 }),
139 };
140 metrics.mark_loop_activity();
141 metrics
142 }
143
144 pub fn connected(&self) -> bool {
146 self.inner.connected.load(Ordering::SeqCst)
147 }
148
149 pub fn last_loop_activity(&self) -> Option<Instant> {
151 self.instant_from_nanos(self.inner.last_loop_activity_nanos.load(Ordering::SeqCst))
152 }
153
154 pub fn last_write_success(&self) -> Option<Instant> {
156 self.instant_from_nanos(self.inner.last_write_success_nanos.load(Ordering::SeqCst))
157 }
158
159 fn instant_from_nanos(&self, nanos: u64) -> Option<Instant> {
160 if nanos == 0 {
161 None
162 } else {
163 self.inner.origin.checked_add(Duration::from_nanos(nanos))
164 }
165 }
166
167 fn now_nanos(&self) -> u64 {
168 (self.inner.origin.elapsed().as_nanos().min(u64::MAX as u128) as u64).max(1)
169 }
170
171 pub(super) fn mark_loop_activity(&self) {
172 self.inner
173 .last_loop_activity_nanos
174 .store(self.now_nanos(), Ordering::SeqCst);
175 }
176
177 pub(super) fn mark_write_success(&self) {
178 let now = self.now_nanos();
179 self.inner
180 .last_loop_activity_nanos
181 .store(now, Ordering::SeqCst);
182 self.inner
183 .last_write_success_nanos
184 .store(now, Ordering::SeqCst);
185 }
186
187 pub(super) fn set_connected(&self, connected: bool) {
188 self.inner.connected.store(connected, Ordering::SeqCst);
189 self.mark_loop_activity();
190 }
191}
192
193struct MetricsDisconnectGuard(FrameSessionMetrics);
194
195impl Drop for MetricsDisconnectGuard {
196 fn drop(&mut self) {
197 self.0.set_connected(false);
198 }
199}
200
201pub struct FrameSession {
221 control: StreamControl,
222 thread: Option<JoinHandle<Result<RunExit>>>,
223 frame_slot: Arc<Mutex<Option<Frame>>>,
224 metrics: FrameSessionMetrics,
225}
226
227impl FrameSession {
228 pub(crate) fn start(
230 mut backend: BackendKind,
231 config: FrameSessionConfig,
232 reconnect_policy: Option<ReconnectPolicy>,
233 ) -> Result<Self> {
234 if !backend.is_connected() {
235 backend.connect()?;
236 }
237
238 let (control_tx, control_rx) = mpsc::channel();
239 let initial_color_delay = if config.color_delay_points > 0 {
240 Duration::from_secs_f64(config.color_delay_points as f64 / config.pps as f64)
241 } else {
242 Duration::ZERO
243 };
244 let control = StreamControl::new(control_tx, initial_color_delay, config.pps);
245 let frame_slot: Arc<Mutex<Option<Frame>>> = Arc::new(Mutex::new(None));
246 let metrics = FrameSessionMetrics::new(backend.is_connected());
247
248 let control_clone = control.clone();
249 let slot_clone = frame_slot.clone();
250 let metrics_clone = metrics.clone();
251
252 let thread = std::thread::Builder::new()
256 .name("laser-frame-scheduler".to_string())
257 .spawn(move || {
258 let _disconnect_guard = MetricsDisconnectGuard(metrics_clone.clone());
259 Self::run_loop(
260 backend,
261 config,
262 control_clone,
263 control_rx,
264 slot_clone,
265 metrics_clone,
266 reconnect_policy,
267 )
268 })
269 .map_err(Error::backend)?;
270
271 Ok(Self {
272 control,
273 thread: Some(thread),
274 frame_slot,
275 metrics,
276 })
277 }
278
279 pub fn control(&self) -> StreamControl {
281 self.control.clone()
282 }
283
284 pub fn send_frame(&self, frame: Frame) {
287 *self.frame_slot.lock().unwrap() = Some(frame);
288 }
289
290 pub fn is_finished(&self) -> bool {
292 self.thread.as_ref().is_some_and(|h| h.is_finished())
293 }
294
295 pub fn metrics(&self) -> FrameSessionMetrics {
297 self.metrics.clone()
298 }
299
300 pub fn join(mut self) -> Result<RunExit> {
302 if let Some(handle) = self.thread.take() {
303 handle
304 .join()
305 .unwrap_or(Err(Error::disconnected("thread panicked")))
306 } else {
307 Ok(RunExit::Stopped)
308 }
309 }
310
311 fn run_loop(
316 mut backend: BackendKind,
317 config: FrameSessionConfig,
318 control: StreamControl,
319 control_rx: mpsc::Receiver<ControlMsg>,
320 frame_slot: Arc<Mutex<Option<Frame>>>,
321 metrics: FrameSessionMetrics,
322 reconnect_policy: Option<ReconnectPolicy>,
323 ) -> Result<RunExit> {
324 let FrameSessionConfig {
325 pps: _,
326 transition_fn,
327 startup_blank,
328 color_delay_points,
329 idle_policy,
330 output_filter,
331 reconnect: _,
332 } = config;
333
334 let mut engine = PresentationEngine::new(transition_fn);
335 if backend.is_frame_swap() {
336 engine.set_frame_capacity(backend.frame_capacity());
337 }
338
339 let initial_buf_capacity = match backend.caps().output_model {
342 OutputModel::UsbFrameSwap => backend.frame_capacity().unwrap_or(0),
343 OutputModel::NetworkFifo | OutputModel::UdpTimed | OutputModel::BlockingFifo => {
344 backend.caps().max_points_per_chunk
345 }
346 };
347 let mut pipeline = SlicePipeline::with_startup_blank(
348 engine,
349 color_delay_points,
350 output_filter,
351 idle_policy,
352 initial_buf_capacity,
353 startup_blank,
354 );
355 pipeline.reset_output_filter(OutputResetReason::SessionStart);
356
357 let expected_frame_swap = backend.is_frame_swap();
358 let source: SourceOwned = if expected_frame_swap {
359 SourceOwned::Frame(Box::new(pipeline))
360 } else {
361 SourceOwned::Fifo(Box::new(pipeline))
362 };
363
364 let validator = Self::reconnect_validator(reconnect_policy.as_ref());
365 if !backend.is_connected() {
366 backend.connect()?;
367 }
368
369 let target_buffer = target_buffer_for_backend(&backend);
370
371 driver::run(DriverInputs {
372 backend,
373 source,
374 control,
375 control_rx,
376 metrics,
377 reconnect_policy,
378 validator,
379 error_sink: Box::new(|_e: Error| { }),
380 target_buffer,
381 drain_timeout: Duration::ZERO,
382 pending_frame: Some(frame_slot),
383 clock: DriverInputs::system_clock(),
384 })
385 }
386
387 fn reconnect_validator(policy: Option<&ReconnectPolicy>) -> driver::ReconnectValidator {
388 let target_id = policy
389 .map(|p| p.target.device_id.clone())
390 .unwrap_or_default();
391 Box::new(move |info: &DacInfo, _backend: &BackendKind, pps: u32| {
392 if pps < info.caps.pps_min || pps > info.caps.pps_max {
393 log::error!(
394 "'{}' PPS {} outside new device range [{}, {}]",
395 target_id,
396 pps,
397 info.caps.pps_min,
398 info.caps.pps_max
399 );
400 return Err(RunExit::Disconnected);
401 }
402 Ok(())
403 })
404 }
405}
406
407impl Drop for FrameSession {
408 fn drop(&mut self) {
409 let _ = self.control.stop();
410 if let Some(handle) = self.thread.take() {
411 let _ = handle.join();
412 }
413 }
414}
415
416pub(super) fn target_buffer_for_backend(backend: &BackendKind) -> Duration {
417 StreamConfig::default_target_buffer_for(&backend.dac_type(), &backend.caps().output_model)
420}