gled-core 2.31.1

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
//! Merging gled's output with Art-Net that arrives for the same recipient,
//! the Art-Net bridge: per channel the highest value wins (HTP), or the
//! latest source wins (LTP), where gled counts as changing while it sends
//! anything but black. Either side counts for a second after it last sent.
//!
//! gled's side is kept per output buffer slot together with the last frame,
//! so a frame neither hashes nor copies its universes while no Art-Net
//! arrives; channel overwrites for universes outside the patch are kept by
//! recipient.

use std::{
    borrow::Cow,
    collections::HashMap,
    time::{Duration, Instant},
};

use gled_gpu::constants::UNIVERSE_BUFFER_SIZE;

use super::Recipient;

/// How long either side counts after it last sent.
const EXPIRY: Duration = Duration::from_secs(1);

type Channels = [u8; UNIVERSE_BUFFER_SIZE as usize];

/// When gled last sent a universe, and last sent it changed.
#[derive(Debug, Clone, Copy)]
struct Gled {
    at: Instant,
    /// The last time gled sent it other than black after black.
    changed: Instant,
    black: bool,
}

impl Gled {
    fn new(now: Instant) -> Self {
        Self {
            at: now,
            changed: now,
            black: false,
        }
    }

    /// gled sends `data` now. Black after black is no change.
    fn send(&mut self, data: &Channels, now: Instant) {
        let black = is_black(data);
        if !(self.black && black) {
            self.changed = now;
        }
        self.black = black;
        self.at = now;
    }

    fn is_fresh(&self, now: Instant) -> bool {
        now.saturating_duration_since(self.at) < EXPIRY
    }
}

/// The last Art-Net that arrived for a recipient.
#[derive(Debug)]
struct Input {
    at: Instant,
    data: Channels,
    htp: bool,
}

#[derive(Debug, Default)]
pub(super) struct Merge {
    inputs: HashMap<Recipient, Input>,
    slots: Vec<Gled>,
    /// The last frame as sent, a universe per slot.
    frame: Vec<u8>,
    /// Universes outside the patch that channel overwrites send.
    extras: HashMap<Recipient, (Gled, Channels)>,
}

impl Merge {
    /// gled sends `data`, slot `slot` of a frame, to `recipient`: what goes
    /// out. Borrowed while no Art-Net arrives for the recipient.
    pub(super) fn slot<'a>(
        &mut self,
        slot: usize,
        recipient: &Recipient,
        data: &'a Channels,
        now: Instant,
    ) -> Cow<'a, Channels> {
        if self.slots.len() <= slot {
            self.slots.resize(slot + 1, Gled::new(now));
        }
        let gled = &mut self.slots[slot];
        gled.send(data, now);
        if self.inputs.is_empty() {
            return Cow::Borrowed(data);
        }
        match self.inputs.get(recipient) {
            Some(input) if is_fresh(input.at, now) => Cow::Owned(merged(gled, data, input)),
            _ => Cow::Borrowed(data),
        }
    }

    /// Keeps the frame just sent, for Art-Net arriving until the next.
    pub(super) fn keep_frame(&mut self, frame: Vec<u8>) {
        self.frame = frame;
    }

    /// gled sends `data` to a recipient outside the patch: what goes out.
    pub(super) fn extra(&mut self, recipient: Recipient, data: Channels, now: Instant) -> Channels {
        let input = self
            .inputs
            .get(&recipient)
            .filter(|input| is_fresh(input.at, now));
        let (gled, kept) = self
            .extras
            .entry(recipient)
            .or_insert_with(|| (Gled::new(now), data));
        gled.send(&data, now);
        *kept = data;
        match input {
            Some(input) => merged(gled, &data, input),
            None => data,
        }
    }

    /// Art-Net arrived for `recipient`, whose universe is slot `slot` of the
    /// patch if it is one: what goes out.
    pub(super) fn input(
        &mut self,
        recipient: Recipient,
        data: Channels,
        htp: bool,
        slot: Option<usize>,
        now: Instant,
    ) -> Channels {
        let input = Input { at: now, data, htp };
        let gled = slot
            .and_then(|slot| {
                let gled = self.slots.get(slot)?;
                let channels = self
                    .frame
                    .as_chunks::<{ UNIVERSE_BUFFER_SIZE as usize }>()
                    .0;
                Some((gled, channels.get(slot)?))
            })
            .or_else(|| self.extras.get(&recipient).map(|(gled, data)| (gled, data)))
            .filter(|(gled, _)| gled.is_fresh(now));
        let out = match gled {
            Some((gled, gled_data)) => merged(gled, gled_data, &input),
            None => data,
        };
        self.inputs.insert(recipient, input);
        out
    }

    /// Forgets the Art-Net and the overwrites nothing sent for a while.
    pub(super) fn expire(&mut self, now: Instant) {
        self.inputs.retain(|_, input| is_fresh(input.at, now));
        self.extras.retain(|_, (gled, _)| gled.is_fresh(now));
    }
}

fn is_fresh(at: Instant, now: Instant) -> bool {
    now.saturating_duration_since(at) < EXPIRY
}

fn merged(gled: &Gled, gled_data: &Channels, input: &Input) -> Channels {
    if input.htp {
        let mut out = *gled_data;
        for (out, input) in out.iter_mut().zip(&input.data) {
            *out = (*out).max(*input);
        }
        out
    } else if input.at > gled.changed {
        input.data
    } else {
        *gled_data
    }
}

fn is_black(data: &Channels) -> bool {
    data.as_chunks::<16>()
        .0
        .iter()
        .all(|chunk| u128::from_ne_bytes(*chunk) == 0)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn node() -> Recipient {
        Recipient::Artnet {
            addr: "10.0.0.1:6454".parse().expect("an address"),
            universe: 3,
        }
    }

    fn lit(value: u8) -> Channels {
        let mut data = [0; 512];
        data[0] = value;
        data[511] = value;
        data
    }

    fn later(now: Instant, millis: u64) -> Instant {
        now + Duration::from_millis(millis)
    }

    #[test]
    fn without_art_net_gled_goes_out_as_it_is() {
        let mut merge = Merge::default();
        let data = lit(9);
        assert!(matches!(
            merge.slot(0, &node(), &data, Instant::now()),
            Cow::Borrowed(sent) if *sent == data
        ));
    }

    #[test]
    fn art_net_without_gled_passes_through() {
        let mut merge = Merge::default();
        assert_eq!(
            merge.input(node(), lit(5), false, Some(0), Instant::now()),
            lit(5)
        );
    }

    #[test]
    fn htp_takes_the_higher_value_of_each_channel() {
        let now = Instant::now();
        let mut merge = Merge::default();
        let mut gled = lit(40);
        gled[7] = 1;
        merge.slot(0, &node(), &gled, now);
        merge.keep_frame(gled.to_vec());
        let mut input = lit(20);
        input[7] = 90;

        let out = merge.input(node(), input, true, Some(0), later(now, 1));

        assert_eq!((out[0], out[7], out[511]), (40, 90, 40));
        let next = merge.slot(0, &node(), &gled, later(now, 2));
        assert_eq!((next[0], next[7]), (40, 90));
    }

    /// LTP: Art-Net that arrives after gled's last change wins; gled wins
    /// again with its next frame that is not black.
    #[test]
    fn ltp_follows_whichever_changed_last() {
        let now = Instant::now();
        let mut merge = Merge::default();
        merge.slot(0, &node(), &lit(40), now);
        merge.keep_frame(lit(40).to_vec());

        assert_eq!(
            merge.input(node(), lit(7), false, Some(0), later(now, 1)),
            lit(7)
        );
        assert_eq!(*merge.slot(0, &node(), &lit(40), later(now, 2)), lit(40));
    }

    /// gled sending black does not count as a change, so a bridged desk
    /// keeps control while gled is dark.
    #[test]
    fn art_net_keeps_ltp_while_gled_stays_black() {
        let now = Instant::now();
        let mut merge = Merge::default();
        merge.slot(0, &node(), &[0; 512], now);
        merge.slot(0, &node(), &[0; 512], later(now, 1));
        merge.input(node(), lit(7), false, Some(0), later(now, 2));

        assert_eq!(*merge.slot(0, &node(), &[0; 512], later(now, 3)), lit(7));
    }

    #[test]
    fn art_net_counts_for_a_second() {
        let now = Instant::now();
        let mut merge = Merge::default();
        merge.input(node(), lit(7), true, Some(0), now);

        let after = later(now, 1_001);
        merge.expire(after);

        assert_eq!(*merge.slot(0, &node(), &lit(1), after), lit(1));
    }

    #[test]
    fn overwrites_outside_the_patch_merge_like_universes() {
        let now = Instant::now();
        let mut merge = Merge::default();
        merge.extra(node(), lit(30), now);

        let out = merge.input(node(), lit(60), true, None, later(now, 1));

        assert_eq!(out, lit(60));
        assert_eq!(merge.extra(node(), lit(90), later(now, 2)), lit(90));
    }

    #[test]
    fn black_is_every_channel_zero() {
        assert!(is_black(&[0; 512]));
        assert!(!is_black(&lit(1)));
        let mut last = [0; 512];
        last[511] = 1;
        assert!(!is_black(&last));
    }
}