use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{self, Receiver};
use std::sync::Arc;
use crate::backend::{BackendKind, Error, Result};
use crate::config::StreamConfig;
use crate::device::{DacCapabilities, DacInfo, DacType};
use crate::discovery::DacDiscovery;
use crate::point::LaserPoint;
use crate::reconnect::{ReconnectPolicy, ReconnectTarget};
use crate::session::{ControlMsg, SessionControl, SessionExit};
pub(crate) mod chunk_producer;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StreamInstant(pub u64);
impl StreamInstant {
pub fn new(points: u64) -> Self {
Self(points)
}
pub fn points(&self) -> u64 {
self.0
}
pub fn as_seconds(&self, pps: u32) -> f64 {
self.0 as f64 / pps as f64
}
#[inline]
pub fn as_secs_f64(&self, pps: u32) -> f64 {
self.as_seconds(pps)
}
pub fn from_seconds(seconds: f64, pps: u32) -> Self {
Self((seconds * pps as f64) as u64)
}
pub fn add_points(&self, points: u64) -> Self {
Self(self.0.saturating_add(points))
}
pub fn sub_points(&self, points: u64) -> Self {
Self(self.0.saturating_sub(points))
}
}
impl std::ops::Add<u64> for StreamInstant {
type Output = Self;
fn add(self, rhs: u64) -> Self::Output {
self.add_points(rhs)
}
}
impl std::ops::Sub<u64> for StreamInstant {
type Output = Self;
fn sub(self, rhs: u64) -> Self::Output {
self.sub_points(rhs)
}
}
impl std::ops::AddAssign<u64> for StreamInstant {
fn add_assign(&mut self, rhs: u64) {
self.0 = self.0.saturating_add(rhs);
}
}
impl std::ops::SubAssign<u64> for StreamInstant {
fn sub_assign(&mut self, rhs: u64) {
self.0 = self.0.saturating_sub(rhs);
}
}
#[derive(Clone, Debug)]
pub struct ChunkRequest {
pub start: StreamInstant,
pub pps: u32,
pub target_points: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChunkResult {
Filled(usize),
Starved,
End,
}
#[derive(Clone, Debug)]
pub struct StreamStatus {
pub connected: bool,
pub scheduled_ahead_points: u64,
pub device_queued_points: Option<u64>,
pub stats: Option<StreamStats>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct StreamStats {
pub underrun_count: u64,
pub reconnect_count: u64,
pub chunks_written: u64,
pub points_written: u64,
}
#[derive(Default)]
pub(crate) struct SharedStreamStats {
underrun_count: AtomicU64,
reconnect_count: AtomicU64,
chunks_written: AtomicU64,
points_written: AtomicU64,
}
impl SharedStreamStats {
pub(crate) fn snapshot(&self) -> StreamStats {
StreamStats {
underrun_count: self.underrun_count.load(Ordering::Relaxed),
reconnect_count: self.reconnect_count.load(Ordering::Relaxed),
chunks_written: self.chunks_written.load(Ordering::Relaxed),
points_written: self.points_written.load(Ordering::Relaxed),
}
}
}
#[derive(Default)]
struct StreamState {
stats: Arc<SharedStreamStats>,
}
impl StreamState {
fn new() -> Self {
Self::default()
}
}
pub struct Stream {
info: DacInfo,
backend: Option<BackendKind>,
config: StreamConfig,
control: SessionControl,
control_rx: Receiver<ControlMsg>,
state: StreamState,
pub(crate) reconnect_policy: Option<ReconnectPolicy>,
pub(crate) reconnect_target: Option<ReconnectTarget>,
}
impl Stream {
pub(crate) fn with_backend(info: DacInfo, backend: BackendKind, config: StreamConfig) -> Self {
let (control_tx, control_rx) = mpsc::channel();
let color_delay = config.color_delay();
let pps = config.pps();
let pps_min = info.caps.pps_min;
let pps_max = info.caps.pps_max;
Self {
info,
backend: Some(backend),
config,
control: SessionControl::new_with_pps_bounds(
control_tx,
color_delay,
pps,
pps_min,
pps_max,
),
control_rx,
state: StreamState::new(),
reconnect_policy: None,
reconnect_target: None,
}
}
pub fn info(&self) -> &DacInfo {
&self.info
}
pub fn config(&self) -> &StreamConfig {
&self.config
}
pub fn control(&self) -> SessionControl {
self.control.clone()
}
pub fn status(&self) -> Result<StreamStatus> {
let buffered = self.estimate_buffer_points();
Ok(StreamStatus {
connected: self.backend.as_ref().is_some_and(|b| b.is_connected()),
scheduled_ahead_points: buffered,
device_queued_points: Some(buffered),
stats: Some(self.state.stats.snapshot()),
})
}
fn shutdown_backend(&mut self) {
let _ = self.control.disarm();
let _ = self.control.stop();
if let Some(b) = &mut self.backend {
let _ = b.set_shutter(false);
let _ = b.stop();
}
}
pub fn stop(&mut self) -> Result<()> {
self.shutdown_backend();
if let Some(b) = &mut self.backend {
b.disconnect()?;
}
Ok(())
}
pub fn into_dac(mut self) -> (Dac, StreamStats) {
self.shutdown_backend();
let backend = self.backend.take();
let stats = self.state.stats.snapshot();
let reconnect_target = self
.reconnect_target
.take()
.or_else(|| self.reconnect_policy.take().map(|p| p.target));
let dac = Dac {
info: self.info.clone(),
backend,
reconnect_target,
};
(dac, stats)
}
pub fn run<F, E>(mut self, producer: F, on_error: E) -> Result<SessionExit>
where
F: FnMut(&ChunkRequest, &mut [LaserPoint]) -> ChunkResult + Send + 'static,
E: FnMut(Error) + Send + 'static,
{
use crate::presentation::driver::{self, DriverInputs, SourceOwned};
use crate::presentation::FrameSessionMetrics;
let mut backend = self
.backend
.take()
.ok_or_else(|| Error::disconnected("backend already consumed"))?;
if backend.is_frame_swap() {
backend.close_and_disconnect();
return Err(Error::invalid_config(
"Stream::run is FIFO-only; use start_frame_session for frame-swap DACs",
));
}
let max_points = self.info.caps.max_points_per_chunk;
let chunk_producer = chunk_producer::ChunkProducer::new(
producer,
self.control.clone(),
self.config.idle_policy().clone(),
self.config.startup_blank(),
max_points,
self.state.stats.clone(),
);
let validator = Self::build_reconnect_validator(self.info.caps.output_model.clone());
let metrics = FrameSessionMetrics::new(true);
let (_dummy_tx, dummy_rx) = mpsc::channel();
let control_rx = std::mem::replace(&mut self.control_rx, dummy_rx);
driver::run(DriverInputs {
backend,
source: SourceOwned::Fifo(Box::new(chunk_producer)),
control: self.control.clone(),
control_rx,
metrics,
reconnect_policy: self.reconnect_policy.take(),
validator,
error_sink: Box::new(on_error),
target_buffer: self.config.target_buffer(),
drain_timeout: self.config.drain_timeout(),
pending_frame: None,
clock: DriverInputs::system_clock(),
})
}
fn build_reconnect_validator(
expected_output_model: crate::device::OutputModel,
) -> crate::presentation::driver::ReconnectValidator {
Box::new(
move |_info: &DacInfo, new_backend: &BackendKind, pps: u32| {
if new_backend.is_frame_swap() {
log::error!("reconnected device is frame-swap, incompatible with streaming");
return Err(SessionExit::IncompatibleDevice);
}
if new_backend.caps().output_model != expected_output_model {
log::error!(
"reconnected device output model {:?} differs from original {:?}; \
the pacing adapter cannot be swapped mid-stream",
new_backend.caps().output_model,
expected_output_model
);
return Err(SessionExit::IncompatibleDevice);
}
if Dac::validate_pps(new_backend.caps(), pps).is_err() {
log::error!("reconnected device PPS range incompatible with stream config");
return Err(SessionExit::IncompatibleDevice);
}
Ok(())
},
)
}
fn estimate_buffer_points(&self) -> u64 {
let pps = self.config.pps();
let now = std::time::Instant::now();
self.backend
.as_ref()
.and_then(|b| b.estimator())
.map_or(0, |e| e.estimated_fullness(now, pps))
}
}
impl Drop for Stream {
fn drop(&mut self) {
let _ = self.stop();
}
}
pub struct Dac {
info: DacInfo,
backend: Option<BackendKind>,
pub(crate) reconnect_target: Option<ReconnectTarget>,
}
impl Dac {
pub fn new(info: DacInfo, backend: BackendKind) -> Self {
Self {
info,
backend: Some(backend),
reconnect_target: None,
}
}
pub fn with_discovery_factory<F>(mut self, factory: F) -> Self
where
F: Fn() -> DacDiscovery + Send + 'static,
{
match self.reconnect_target {
Some(ref mut target) => {
target.discovery_factory = Some(Box::new(factory));
}
None => {
self.reconnect_target = Some(ReconnectTarget {
device_id: self.info.id.clone(),
discovery_factory: Some(Box::new(factory)),
});
}
}
self
}
pub fn info(&self) -> &DacInfo {
&self.info
}
pub fn id(&self) -> &str {
&self.info.id
}
pub fn name(&self) -> &str {
&self.info.name
}
pub fn kind(&self) -> &DacType {
&self.info.kind
}
pub fn caps(&self) -> &DacCapabilities {
&self.info.caps
}
pub fn has_backend(&self) -> bool {
self.backend.is_some()
}
pub(crate) fn into_backend(mut self) -> Option<BackendKind> {
self.backend.take()
}
pub fn is_connected(&self) -> bool {
self.backend.as_ref().is_some_and(|b| b.is_connected())
}
pub fn start_stream(mut self, mut cfg: StreamConfig) -> Result<(Stream, DacInfo)> {
let reconnect_config = cfg.take_reconnect();
let mut backend = self.backend.take().ok_or_else(|| {
Error::invalid_config("device backend has already been used for a stream")
})?;
if let Err(err) = backend.validate_output_model() {
backend.close_and_disconnect();
return Err(err);
}
if backend.is_frame_swap() {
backend.close_and_disconnect();
return Err(Error::invalid_config(
"streaming is not supported on frame-swap DACs (e.g. Helios); \
use start_frame_session() instead",
));
}
if !backend.is_connected() {
if let Err(err) = backend.connect() {
backend.close_and_disconnect();
return Err(err);
}
}
if let Err(err) = backend.set_shutter(false) {
backend.close_and_disconnect();
return Err(err);
}
self.info.caps = backend.caps().clone();
let cfg = Self::apply_backend_buffer_defaults(&self.info, cfg);
if let Err(err) = Self::validate_pps(&self.info.caps, cfg.pps()) {
backend.close_and_disconnect();
return Err(err);
}
let mut stream = Stream::with_backend(self.info.clone(), backend, cfg);
stream.reconnect_target = self.reconnect_target.take();
if let Some(rc) = reconnect_config {
let Some(target) = stream.reconnect_target.take() else {
if let Some(mut backend) = stream.backend.take() {
backend.close_and_disconnect();
}
return Err(Error::invalid_config("reconnect requires a reconnect target — use open_device(), open_device_with(), or Dac::with_discovery_factory()"));
};
stream.reconnect_policy = Some(ReconnectPolicy::new(rc, target));
}
Ok((stream, self.info))
}
fn apply_backend_buffer_defaults(info: &DacInfo, mut cfg: StreamConfig) -> StreamConfig {
if cfg.target_buffer() == StreamConfig::DEFAULT_TARGET_BUFFER {
cfg.set_target_buffer(StreamConfig::default_target_buffer_for(
&info.kind,
&info.caps.output_model,
));
}
cfg
}
fn validate_pps(caps: &DacCapabilities, pps: u32) -> Result<()> {
if pps < caps.pps_min || pps > caps.pps_max {
return Err(Error::invalid_config(format!(
"PPS {} is outside device range [{}, {}]",
pps, caps.pps_min, caps.pps_max
)));
}
Ok(())
}
pub fn start_frame_session(
mut self,
mut config: crate::presentation::FrameSessionConfig,
) -> Result<(crate::presentation::FrameSession, DacInfo)> {
let reconnect_config = config.take_reconnect();
let mut backend = self.backend.take().ok_or_else(|| {
Error::invalid_config("device backend has already been used for a session")
})?;
if let Err(err) = backend.validate_output_model() {
backend.close_and_disconnect();
return Err(err);
}
if !backend.is_connected() {
if let Err(err) = backend.connect() {
backend.close_and_disconnect();
return Err(err);
}
}
if let Err(err) = backend.set_shutter(false) {
backend.close_and_disconnect();
return Err(err);
}
if let Err(err) = Self::validate_pps(backend.caps(), config.pps()) {
backend.close_and_disconnect();
return Err(err);
}
self.info.caps = backend.caps().clone();
let reconnect_policy = match reconnect_config {
Some(rc) => {
let Some(target) = self.reconnect_target.take() else {
backend.close_and_disconnect();
return Err(Error::invalid_config("reconnect requires a reconnect target — use open_device(), open_device_with(), or Dac::with_discovery_factory()"));
};
Some(ReconnectPolicy::new(rc, target))
}
None => None,
};
let session = crate::presentation::FrameSession::start(backend, config, reconnect_policy)?;
Ok((session, self.info))
}
}
#[cfg(test)]
mod tests;