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()
254 .name("laser-frame-scheduler".to_string())
255 .spawn(move || {
256 if let Err(e) = thread_priority::set_current_thread_priority(
262 thread_priority::ThreadPriority::Max,
263 ) {
264 log::warn!("laser-frame-scheduler: could not raise thread priority: {e:?}");
265 }
266 let _disconnect_guard = MetricsDisconnectGuard(metrics_clone.clone());
267 Self::run_loop(
268 backend,
269 config,
270 control_clone,
271 control_rx,
272 slot_clone,
273 metrics_clone,
274 reconnect_policy,
275 )
276 })
277 .map_err(Error::backend)?;
278
279 Ok(Self {
280 control,
281 thread: Some(thread),
282 frame_slot,
283 metrics,
284 })
285 }
286
287 pub fn control(&self) -> StreamControl {
289 self.control.clone()
290 }
291
292 pub fn send_frame(&self, frame: Frame) {
295 *self.frame_slot.lock().unwrap() = Some(frame);
296 }
297
298 pub fn is_finished(&self) -> bool {
300 self.thread.as_ref().is_some_and(|h| h.is_finished())
301 }
302
303 pub fn metrics(&self) -> FrameSessionMetrics {
305 self.metrics.clone()
306 }
307
308 pub fn join(mut self) -> Result<RunExit> {
310 if let Some(handle) = self.thread.take() {
311 handle
312 .join()
313 .unwrap_or(Err(Error::disconnected("thread panicked")))
314 } else {
315 Ok(RunExit::Stopped)
316 }
317 }
318
319 fn run_loop(
324 mut backend: BackendKind,
325 config: FrameSessionConfig,
326 control: StreamControl,
327 control_rx: mpsc::Receiver<ControlMsg>,
328 frame_slot: Arc<Mutex<Option<Frame>>>,
329 metrics: FrameSessionMetrics,
330 reconnect_policy: Option<ReconnectPolicy>,
331 ) -> Result<RunExit> {
332 let FrameSessionConfig {
333 pps: _,
334 transition_fn,
335 startup_blank,
336 color_delay_points,
337 idle_policy,
338 output_filter,
339 reconnect: _,
340 } = config;
341
342 let mut engine = PresentationEngine::new(transition_fn);
343 if backend.is_frame_swap() {
344 engine.set_frame_capacity(backend.frame_capacity());
345 }
346
347 let initial_buf_capacity = match backend.caps().output_model {
350 OutputModel::UsbFrameSwap => backend.frame_capacity().unwrap_or(0),
351 OutputModel::NetworkFifo | OutputModel::UdpTimed | OutputModel::BlockingFifo => {
352 backend.caps().max_points_per_chunk
353 }
354 };
355 let mut pipeline = SlicePipeline::with_startup_blank(
356 engine,
357 color_delay_points,
358 output_filter,
359 idle_policy,
360 initial_buf_capacity,
361 startup_blank,
362 );
363 pipeline.reset_output_filter(OutputResetReason::SessionStart);
364
365 let expected_frame_swap = backend.is_frame_swap();
366 let source: SourceOwned = if expected_frame_swap {
367 SourceOwned::Frame(Box::new(pipeline))
368 } else {
369 SourceOwned::Fifo(Box::new(pipeline))
370 };
371
372 let validator = Self::reconnect_validator(reconnect_policy.as_ref());
373 if !backend.is_connected() {
374 backend.connect()?;
375 }
376
377 let target_buffer = target_buffer_for_backend(&backend);
378
379 driver::run(DriverInputs {
380 backend,
381 source,
382 control,
383 control_rx,
384 metrics,
385 reconnect_policy,
386 validator,
387 error_sink: Box::new(|_e: Error| { }),
388 target_buffer,
389 drain_timeout: Duration::ZERO,
390 pending_frame: Some(frame_slot),
391 clock: DriverInputs::system_clock(),
392 })
393 }
394
395 fn reconnect_validator(policy: Option<&ReconnectPolicy>) -> driver::ReconnectValidator {
396 let target_id = policy
397 .map(|p| p.target.device_id.clone())
398 .unwrap_or_default();
399 Box::new(move |info: &DacInfo, _backend: &BackendKind, pps: u32| {
400 if pps < info.caps.pps_min || pps > info.caps.pps_max {
401 log::error!(
402 "'{}' PPS {} outside new device range [{}, {}]",
403 target_id,
404 pps,
405 info.caps.pps_min,
406 info.caps.pps_max
407 );
408 return Err(RunExit::Disconnected);
409 }
410 Ok(())
411 })
412 }
413}
414
415impl Drop for FrameSession {
416 fn drop(&mut self) {
417 let _ = self.control.stop();
418 if let Some(handle) = self.thread.take() {
419 let _ = handle.join();
420 }
421 }
422}
423
424pub(super) fn target_buffer_for_backend(backend: &BackendKind) -> Duration {
425 StreamConfig::default_target_buffer_for(&backend.dac_type(), &backend.caps().output_model)
428}