mod artdmx;
#[cfg(test)]
mod bench;
pub(crate) mod enttec_usb_pro;
mod merge;
mod routes;
mod senders;
pub(crate) use crate::storage::asset::output_device::routing::Recipient;
use anyhow::{Context, Result};
use kanal::{Receiver, Sender, bounded};
use std::{
net::UdpSocket,
sync::Arc,
thread,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use tracing::debug;
use crate::{
input::artnet::ARTNET_SOCKET,
pipeline::extract_output::{OutputFrame, OutputFrames},
storage::collections::Collections,
};
use gled_gpu::constants::UNIVERSE_BUFFER_SIZE;
use gled_model::asset::{output_device::routing::OutputRouting, project::io::ChannelOverwrites};
use merge::Merge;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use routes::Routes;
use senders::{Batch, Dropped, Senders, batch_for};
const INPUT_CAPACITY: usize = 1024;
const EXPIRY_INTERVAL: Duration = Duration::from_millis(250);
type Channels = [u8; UNIVERSE_BUFFER_SIZE as usize];
static CHANNEL_OVERWRITES: Lazy<Mutex<ChannelOverwrites>> =
Lazy::new(|| Mutex::new(ChannelOverwrites::default()));
pub(crate) static HOVERED_OUTPUT_ROUTING: Lazy<Mutex<Option<OutputRouting>>> =
Lazy::new(|| Mutex::new(None));
pub(crate) fn current_channel_overwrites() -> ChannelOverwrites {
CHANNEL_OVERWRITES.lock().clone()
}
pub(crate) fn set_channel_overwrites(overwrites: ChannelOverwrites) {
*CHANNEL_OVERWRITES.lock() = overwrites;
}
#[derive(Debug)]
pub(crate) struct ArtnetInput {
pub(crate) recipient: Recipient,
pub(crate) data: Channels,
pub(crate) htp: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct ArtnetInputs {
inputs: Sender<ArtnetInput>,
bell: Sender<()>,
}
impl ArtnetInputs {
pub(crate) fn send(&self, input: ArtnetInput) {
if self.inputs.send(input).is_ok() {
let _ = self.bell.try_send(());
}
}
}
pub(crate) fn start(frames: OutputFrames) -> Result<ArtnetInputs> {
let OutputFrames { frames, wake, bell } = frames;
enttec_usb_pro::start();
debug!("Spawning output threads");
let socket = ARTNET_SOCKET.clone();
match socket.set_broadcast(true) {
Ok(_) => debug!("Activated sending to broadcast"),
Err(e) => debug!("Could not activate sending to broadcast: {e}"),
}
match socket.set_nonblocking(true) {
Ok(_) => debug!("Activated non-blocking mode"),
Err(e) => debug!("Could not activate non-blocking mode: {e}"),
};
let senders = Senders::start(&socket)?;
let (input_sender, inputs) = bounded(INPUT_CAPACITY);
thread::Builder::new()
.name("gled:output:prepare".to_owned())
.spawn(move || {
#[cfg(feature = "profiling")]
profiling::register_thread!("output:prepare");
Output {
frames,
wake,
inputs,
socket,
senders,
collections: None,
routes: Routes::default(),
merge: Merge::default(),
expired: Instant::now(),
packet: Vec::new(),
dropped: Dropped::default(),
}
.run();
})
.context("Could not spawn the output thread")?;
Ok(ArtnetInputs {
inputs: input_sender,
bell,
})
}
struct Output {
frames: Receiver<OutputFrame>,
wake: Receiver<()>,
inputs: Receiver<ArtnetInput>,
socket: Arc<UdpSocket>,
senders: Senders,
collections: Option<Collections>,
routes: Routes,
merge: Merge,
expired: Instant,
packet: Vec<u8>,
dropped: Dropped,
}
impl Output {
fn run(mut self) {
while self.wake.recv().is_ok() {
self.bridge_inputs();
while let Ok(Some(frame)) = self.frames.try_recv() {
self.send_frame(frame);
self.bridge_inputs();
}
let now = Instant::now();
if now.saturating_duration_since(self.expired) >= EXPIRY_INTERVAL {
self.merge.expire(now);
self.expired = now;
}
}
debug!("Nothing hands the output frames any more, stopping it");
}
fn bridge_inputs(&mut self) {
while let Ok(Some(input)) = self.inputs.try_recv() {
let slot = self.routes.slot_of(&input.recipient);
let data = self.merge.input(
input.recipient.clone(),
input.data,
input.htp,
slot,
Instant::now(),
);
match &input.recipient {
Recipient::Artnet { addr, universe } => {
self.packet.clear();
if artdmx::write(&mut self.packet, *universe, &data) {
let bytes =
senders::send(&self.socket, &self.packet, *addr, &mut self.dropped);
crate::network_stats::add_outgoing_bytes(bytes);
}
}
Recipient::EnttecDmxUsbPro { serial_number } => {
enttec_usb_pro::send(serial_number.clone(), data);
}
}
}
}
#[cfg_attr(feature = "profiling", profiling::function)]
fn send_frame(&mut self, frame: OutputFrame) {
let Self {
senders,
collections,
routes,
merge,
..
} = self;
let OutputFrame { mut data, config } = frame;
let collections = collections.get_or_insert_with(Collections::default);
collections.update();
routes.update(&config, collections);
let hovered = HOVERED_OUTPUT_ROUTING.lock().clone();
let mut overwrites = current_channel_overwrites();
let now = Instant::now();
let universes = data.as_chunks_mut::<{ UNIVERSE_BUFFER_SIZE as usize }>().0;
let slots = universes.len().min(routes.len());
let mut batches = senders.take(slots);
let count = batches.len();
for (slot, values) in universes.iter_mut().take(slots).enumerate() {
let Some(route) = routes.slot(slot) else {
continue;
};
if Some(&route.routing) == hovered.as_ref() {
continue;
}
config.color_orders.correct(route.universe, values);
overwrites.overwrite_data(route.device, route.routing.universe, values);
let Some(recipient) = &route.recipient else {
continue;
};
let out = merge.slot(slot, recipient, values, now);
deliver(&mut batches[batch_for(slot, slots, count)], recipient, &out);
}
let last = &mut batches[count - 1];
if let Some(recipient) = hovered.and_then(|routing| routing.recipient(collections)) {
deliver(last, &recipient, &[blink(); UNIVERSE_BUFFER_SIZE as usize]);
}
for (routing, values) in overwrites.overwritten_universes() {
if let Some(recipient) = routing.recipient(collections) {
let out = merge.extra(recipient.clone(), values, now);
deliver(last, &recipient, &out);
}
}
senders.send(batches);
merge.keep_frame(data);
}
}
fn deliver(batch: &mut Batch, recipient: &Recipient, data: &Channels) {
match recipient {
Recipient::Artnet { addr, universe } => batch.push(*addr, *universe, data),
Recipient::EnttecDmxUsbPro { serial_number } => {
enttec_usb_pro::send(serial_number.clone(), *data);
}
}
}
fn blink() -> u8 {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |time| time.as_millis());
(millis % 512).abs_diff(256) as u8
}