Skip to main content

driver/
lib.rs

1//! Driving a wall of panels: turn frames into Colorlight packets and send them.
2//!
3//! This is the join between the topology in `wall`, the wire format in
4//! `colorlight`, and the transport in `rawlink`. Every content command goes
5//! through [`Wall::show`], so there is one frame recipe to measure.
6
7use anyhow::{Context, Result};
8use wall::{Canvas, Frame};
9use rawlink::Link;
10use std::ops::Range;
11use std::time::{Duration, Instant};
12
13/// How long `recv` may block; only replies to the layout frame are ever read.
14const RECV_TIMEOUT: Duration = Duration::from_millis(200);
15
16/// Per-refresh timing. The defaults were measured on the bench
17/// (docs/rendering.md); change them only with a measurement.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct Timing {
20    /// Latch frames after the rows. One never starts the display; two decays
21    /// into noise on a ~10 s period; three hold.
22    pub latches: u32,
23    /// Pause between the last row and the first latch. Without it the card
24    /// latches before the last row is stored and that row flickers.
25    pub latch_gap: Duration,
26    /// Pause between row packets. The card's receive FIFO is 1 KB, so a
27    /// line-rate burst can drop its tail; zero measured fine.
28    pub row_gap: Duration,
29}
30
31impl Default for Timing {
32    fn default() -> Self {
33        Self {
34            latches: 3,
35            latch_gap: Duration::from_micros(500),
36            row_gap: Duration::ZERO,
37        }
38    }
39}
40
41/// How a wall should be driven.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct Settings {
44    pub brightness: u8,
45    pub color_order: colorlight::ColorOrder,
46    /// Send a layout frame before the first frame. Off by default: a
47    /// provisioned card takes its control area from EEPROM, and the layout
48    /// frame blanks it.
49    pub announce_layout: bool,
50    pub timing: Timing,
51}
52
53impl Default for Settings {
54    fn default() -> Self {
55        Self {
56            brightness: 255,
57            color_order: colorlight::ColorOrder::Bgr,
58            announce_layout: false,
59            timing: Timing::default(),
60        }
61    }
62}
63
64/// Where a [`Wall`] sends its frames: the raw link in production, a
65/// recording sink in tests so the frame recipe can be pinned offline.
66pub trait FrameSink {
67    /// Send one raw Ethernet frame.
68    ///
69    /// # Errors
70    /// Fails if the frame cannot be sent.
71    fn send(&mut self, frame: &[u8]) -> Result<()>;
72}
73
74impl FrameSink for Link {
75    fn send(&mut self, frame: &[u8]) -> Result<()> {
76        Self::send(self, frame)
77    }
78}
79
80/// A wall of panels on one network interface.
81pub struct Wall<S: FrameSink = Link> {
82    dev: S,
83    canvas: Canvas,
84    settings: Settings,
85    announced: bool,
86    frames_sent: u64,
87    /// The screen framebuffer, reused across refreshes.
88    screen: Frame,
89    /// Scratch buffer for the row packet being sent.
90    packet: Vec<u8>,
91    brightness_frame: [u8; 77],
92    sync_frame: [u8; 112],
93}
94
95impl Wall<Link> {
96    /// Open the interface and bind it to a canvas.
97    ///
98    /// # Errors
99    /// Fails if the interface cannot be opened, or the canvas is inconsistent.
100    pub fn open(iface: &str, canvas: Canvas, settings: Settings) -> Result<Self> {
101        canvas.validate()?;
102        let dev = Link::open(iface, RECV_TIMEOUT).with_context(|| format!("open {iface}"))?;
103        Self::with_sink(dev, canvas, settings)
104    }
105}
106
107impl<S: FrameSink> Wall<S> {
108    /// Bind a canvas to any frame sink.
109    ///
110    /// # Errors
111    /// Fails if the canvas is inconsistent or too large for the row packet's
112    /// u16 coordinates (every receiver lies inside it, so they fit too).
113    pub fn with_sink(dev: S, canvas: Canvas, settings: Settings) -> Result<Self> {
114        canvas.validate()?;
115        fits_u16(canvas.width, canvas.height).context("canvas is larger than 65535 px")?;
116        Ok(Self {
117            dev,
118            screen: canvas.screen_frame(),
119            canvas,
120            settings,
121            announced: false,
122            frames_sent: 0,
123            packet: Vec::new(),
124            brightness_frame: colorlight::brightness(settings.brightness),
125            sync_frame: colorlight::sync(settings.brightness),
126        })
127    }
128
129    #[must_use]
130    pub const fn canvas(&self) -> &Canvas {
131        &self.canvas
132    }
133
134    #[must_use]
135    pub const fn frames_sent(&self) -> u64 {
136        self.frames_sent
137    }
138
139    #[must_use]
140    pub const fn settings(&self) -> &Settings {
141        &self.settings
142    }
143
144    /// Change the brightness every later refresh carries. Nothing is sent
145    /// here; the cached brightness and latch frames are rebuilt.
146    pub fn set_brightness(&mut self, brightness: u8) {
147        self.set_gains(brightness, [brightness; 3]);
148    }
149
150    /// [`set_brightness`](Self::set_brightness) with the latch frame's three
151    /// channel gains given separately (`colorlight::sync_gains`); the brightness
152    /// frame carries only the master.
153    pub fn set_gains(&mut self, brightness: u8, gains: [u8; 3]) {
154        self.settings.brightness = brightness;
155        self.brightness_frame = colorlight::brightness(brightness);
156        self.sync_frame = colorlight::sync_gains(brightness, gains);
157    }
158
159    /// Tell every receiver its own window and the size of the whole wall.
160    /// The `as u16` casts are lossless: sizes were checked in `with_sink`.
161    ///
162    /// # Errors
163    /// Fails if a frame cannot be sent.
164    pub fn announce_layout(&mut self) -> Result<()> {
165        for r in &self.canvas.receivers {
166            let frame = colorlight::set_layout(
167                r.index,
168                r.width as u16,
169                r.height as u16,
170                r.x as u16,
171                r.y as u16,
172                self.canvas.width as u16,
173                self.canvas.height as u16,
174            );
175            self.dev.send(&frame)?;
176        }
177        self.announced = true;
178        Ok(())
179    }
180
181    /// Render one canvas frame onto the screen and push the screen to the
182    /// chain: brightness, one row packet per screen row (chunked at 497
183    /// pixels), the latch gap, the latches. Row and pixel offset are screen
184    /// coordinates; every card keeps its own window of them
185    /// (docs/receiver-identity.md), so the stream is the same however many
186    /// cards listen.
187    ///
188    /// A card fresh from arming never starts when the latch leads the rows;
189    /// once woken either order works (docs/rendering.md).
190    ///
191    /// # Errors
192    /// Fails if a frame cannot be sent.
193    pub fn show(&mut self, frame: &Frame) -> Result<()> {
194        let all = 0..self.canvas.height;
195        self.show_rows(frame, all)
196    }
197
198    /// [`show`](Self::show) sending only the screen rows in `rows` (clipped
199    /// to the screen) between the brightness frame and the latches. Rows are
200    /// addressed and the card keeps its frame memory, so the rest of the
201    /// picture stays as last sent; an empty range sends brightness and
202    /// latches alone. The card's own scan bounds how fast a band can change.
203    ///
204    /// # Errors
205    /// Fails if a frame cannot be sent.
206    pub fn show_rows(&mut self, frame: &Frame, rows: Range<u32>) -> Result<()> {
207        if self.settings.announce_layout && !self.announced {
208            self.announce_layout()?;
209        }
210        let Settings {
211            color_order, timing, ..
212        } = self.settings;
213        self.dev.send(&self.brightness_frame)?;
214
215        self.canvas.render_into(frame, &mut self.screen);
216        let height = self.screen.height;
217        for (i, y) in (rows.start.min(height)..rows.end.min(height)).enumerate() {
218            if i > 0 && !timing.row_gap.is_zero() {
219                std::thread::sleep(timing.row_gap);
220            }
221            let row = self.screen.row(y);
222            for (j, chunk) in row.chunks(colorlight::MAX_PIXELS_PER_PACKET).enumerate() {
223                let offset = j * colorlight::MAX_PIXELS_PER_PACKET;
224                colorlight::pixel_row_into(&mut self.packet, y as u16, offset as u16, chunk, color_order);
225                self.dev.send(&self.packet)?;
226            }
227        }
228
229        std::thread::sleep(timing.latch_gap);
230        for _ in 0..timing.latches {
231            self.dev.send(&self.sync_frame)?;
232        }
233        self.frames_sent += 1;
234        Ok(())
235    }
236
237    /// Blank every panel.
238    ///
239    /// # Errors
240    /// Fails if a frame cannot be sent.
241    pub fn blank(&mut self) -> Result<()> {
242        let black = Frame::black(self.canvas.width, self.canvas.height);
243        self.show(&black)
244    }
245}
246
247fn fits_u16(w: u32, h: u32) -> Result<()> {
248    u16::try_from(w)?;
249    u16::try_from(h)?;
250    Ok(())
251}
252
253/// Paces a loop to a target frame rate, reporting the rate actually achieved.
254pub struct Pacer {
255    period: Duration,
256    next: Instant,
257    started: Instant,
258    frames: u64,
259}
260
261impl Pacer {
262    #[must_use]
263    pub fn new(fps: u32) -> Self {
264        let period = Duration::from_secs_f64(1.0 / f64::from(fps.max(1)));
265        let now = Instant::now();
266        Self {
267            period,
268            next: now,
269            started: now,
270            frames: 0,
271        }
272    }
273
274    /// Sleep until the next frame is due.
275    pub fn wait(&mut self) {
276        self.frames += 1;
277        self.next += self.period;
278        let now = Instant::now();
279        if self.next > now {
280            std::thread::sleep(self.next - now);
281        } else {
282            // Running behind; give up on catching up rather than spiralling.
283            self.next = now;
284        }
285    }
286
287    /// Frames per second actually achieved so far.
288    #[must_use]
289    pub fn achieved_fps(&self) -> f64 {
290        let secs = self.started.elapsed().as_secs_f64();
291        if secs > 0.0 {
292            self.frames as f64 / secs
293        } else {
294            0.0
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    impl FrameSink for Vec<Vec<u8>> {
304        fn send(&mut self, frame: &[u8]) -> Result<()> {
305            self.push(frame.to_vec());
306            Ok(())
307        }
308    }
309
310    fn gradient(w: u32, h: u32) -> Frame {
311        let mut f = Frame::black(w, h);
312        for y in 0..h {
313            for x in 0..w {
314                f.set_pixel(x, y, [x as u8, y as u8, (x ^ y) as u8]);
315            }
316        }
317        f
318    }
319
320    /// No sleeps, so the recipe tests run instantly; the counts are the
321    /// measured defaults.
322    fn quick() -> Settings {
323        Settings {
324            timing: Timing {
325                latch_gap: Duration::ZERO,
326                ..Timing::default()
327            },
328            ..Settings::default()
329        }
330    }
331
332    fn be16(f: &[u8], at: usize) -> u16 {
333        u16::from_be_bytes([f[at], f[at + 1]])
334    }
335
336    #[test]
337    fn pacer_reports_a_plausible_rate() {
338        let mut p = Pacer::new(1000);
339        for _ in 0..5 {
340            p.wait();
341        }
342        assert!(p.achieved_fps() > 0.0);
343    }
344
345    #[test]
346    fn settings_default_to_the_measured_recipe() {
347        let s = Settings::default();
348        assert_eq!(s.brightness, 255);
349        assert!(!s.announce_layout);
350        assert_eq!(
351            s.timing,
352            Timing {
353                latches: 3,
354                latch_gap: Duration::from_micros(500),
355                row_gap: Duration::ZERO,
356            }
357        );
358    }
359
360    #[test]
361    fn a_refresh_is_brightness_then_rows_then_three_latches() {
362        let settings = quick();
363        let mut wall = Wall::with_sink(Vec::new(), Canvas::single(128, 64), settings).unwrap();
364        let frame = gradient(128, 64);
365        wall.show(&frame).unwrap();
366        let sent = &wall.dev;
367
368        assert_eq!(sent.len(), 1 + 64 + 3);
369        assert_eq!(sent[0].len(), 77);
370        assert_eq!(&sent[0][12..17], &[0x0a, 0xff, 0xff, 0xff, 0xff]);
371        for (y, f) in sent[1..65].iter().enumerate() {
372            assert_eq!(f[12], 0x55);
373            assert_eq!(be16(f, 13), y as u16, "row");
374            assert_eq!(be16(f, 15), 0, "offset");
375            assert_eq!(be16(f, 17), 128, "count");
376            assert_eq!(&f[19..21], &[0x08, 0x88]);
377            assert_eq!(f.len(), 21 + 128 * 3);
378            assert_eq!(f, &colorlight::pixel_row(y as u16, 0, frame.row(y as u32), settings.color_order));
379        }
380        for f in &sent[65..] {
381            assert_eq!(f.len(), 112);
382            assert_eq!(&f[12..14], &[0x01, 0x07]);
383            assert_eq!(f[35], 0xff);
384        }
385        assert_eq!(wall.frames_sent(), 1);
386    }
387
388    #[test]
389    fn wide_rows_are_chunked_at_the_packet_limit() {
390        let settings = Settings {
391            brightness: 40,
392            color_order: colorlight::ColorOrder::Rgb,
393            ..quick()
394        };
395        let mut wall = Wall::with_sink(Vec::new(), Canvas::single(1000, 2), settings).unwrap();
396        let frame = gradient(1000, 2);
397        wall.show(&frame).unwrap();
398        let sent = &wall.dev;
399
400        assert_eq!(sent.len(), 1 + 2 * 3 + 3);
401        assert_eq!(&sent[0][13..17], &[40, 40, 40, 0xff]);
402        let mut rows = sent[1..7].iter();
403        for y in 0..2u16 {
404            for (offset, count) in [(0, 497), (497, 497), (994, 6)] {
405                let f = rows.next().unwrap();
406                assert_eq!((f[12], be16(f, 13), be16(f, 15), be16(f, 17)), (0x55, y, offset, count));
407                let px = &frame.row(u32::from(y))[usize::from(offset)..][..usize::from(count)];
408                assert_eq!(f, &colorlight::pixel_row(y, offset, px, settings.color_order));
409            }
410        }
411        assert_eq!(sent[7][35], 40);
412        assert_eq!(&sent[7][38..41], &[40; 3]);
413    }
414
415    #[test]
416    fn two_cards_share_one_screen_stream_and_the_layout_frame_leads_when_asked() {
417        let settings = Settings {
418            announce_layout: true,
419            ..quick()
420        };
421        let canvas = Canvas::cards(8, 4, 2, 1);
422        let mut wall = Wall::with_sink(Vec::new(), canvas, settings).unwrap();
423        let frame = gradient(16, 4);
424        wall.show(&frame).unwrap();
425        wall.show(&frame).unwrap();
426        let sent = &wall.dev;
427
428        // Layout per card, then two refreshes of brightness + 4 screen rows
429        // + 3 latches: the second card is told its window, not sent its own rows.
430        assert_eq!(sent.len(), 2 + 2 * (1 + 4 + 3));
431        assert_eq!(sent[0], colorlight::set_layout(0, 8, 4, 0, 0, 16, 4));
432        assert_eq!(sent[1], colorlight::set_layout(1, 8, 4, 8, 0, 16, 4));
433        for refresh in [&sent[2..10], &sent[10..18]] {
434            assert_eq!(refresh[0][12], 0x0a);
435            for (y, f) in refresh[1..5].iter().enumerate() {
436                assert_eq!(be16(f, 17), 16, "the whole screen row");
437                assert_eq!(f, &colorlight::pixel_row(y as u16, 0, frame.row(y as u32), settings.color_order));
438            }
439            assert!(refresh[5..].iter().all(|f| f[12] == 0x01));
440        }
441    }
442
443    #[test]
444    fn show_rows_sends_only_that_band_between_brightness_and_latches() {
445        let settings = quick();
446        let mut wall = Wall::with_sink(Vec::new(), Canvas::single(128, 64), settings).unwrap();
447        let frame = gradient(128, 64);
448        wall.show_rows(&frame, 10..13).unwrap();
449        let sent = &wall.dev;
450
451        assert_eq!(sent.len(), 1 + 3 + 3);
452        assert_eq!(sent[0], colorlight::brightness(255));
453        for (f, y) in sent[1..4].iter().zip(10u16..) {
454            assert_eq!(be16(f, 13), y, "row");
455            assert_eq!(f, &colorlight::pixel_row(y, 0, frame.row(u32::from(y)), settings.color_order));
456        }
457        assert!(sent[4..].iter().all(|f| f == &colorlight::sync(255)));
458        assert_eq!(wall.frames_sent(), 1);
459
460        // Clipped to the screen; an empty band still latches.
461        wall.dev.clear();
462        wall.show_rows(&frame, 60..100).unwrap();
463        assert_eq!(wall.dev.len(), 1 + 4 + 3);
464        assert_eq!(be16(&wall.dev[4], 13), 63);
465        wall.dev.clear();
466        wall.show_rows(&frame, 0..0).unwrap();
467        assert_eq!(wall.dev.len(), 1 + 3);
468        assert_eq!(wall.frames_sent(), 3);
469    }
470
471    #[test]
472    fn show_is_show_rows_over_the_whole_screen() {
473        let frame = gradient(1000, 2);
474        let mut whole = Wall::with_sink(Vec::new(), Canvas::single(1000, 2), quick()).unwrap();
475        let mut band = Wall::with_sink(Vec::new(), Canvas::single(1000, 2), quick()).unwrap();
476        whole.show(&frame).unwrap();
477        band.show_rows(&frame, 0..2).unwrap();
478        assert_eq!(whole.dev, band.dev);
479    }
480
481    #[test]
482    fn set_brightness_rebuilds_the_brightness_and_latch_frames() {
483        let mut wall = Wall::with_sink(Vec::new(), Canvas::single(8, 1), quick()).unwrap();
484        let frame = gradient(8, 1);
485        wall.show(&frame).unwrap();
486        wall.set_brightness(40);
487        assert_eq!(wall.settings().brightness, 40);
488        wall.show(&frame).unwrap();
489        wall.set_gains(40, [10, 20, 30]);
490        wall.show(&frame).unwrap();
491        let sent = &wall.dev;
492
493        // Three refreshes of brightness + 1 row + 3 latches.
494        assert_eq!(sent.len(), 3 * 5);
495        assert_eq!(sent[0], colorlight::brightness(255));
496        assert_eq!(sent[2], colorlight::sync(255));
497        assert_eq!(sent[5], colorlight::brightness(40));
498        assert_eq!(&sent[5][13..17], &[40, 40, 40, 0xff]);
499        assert!(sent[7..10].iter().all(|f| f == &colorlight::sync(40)));
500        assert_eq!(sent[10], colorlight::brightness(40));
501        assert!(sent[12..15].iter().all(|f| f == &colorlight::sync_gains(40, [10, 20, 30])));
502        assert_eq!(&sent[12][38..41], &[10, 20, 30]);
503        assert_eq!(sent[6], sent[11], "the row packet does not carry brightness");
504    }
505
506    #[test]
507    fn a_card_placed_further_along_the_screen_gets_its_pixels_at_that_offset() {
508        let mut canvas = Canvas::cards(8, 4, 2, 1);
509        canvas.receivers.swap(0, 1);
510        canvas.receivers[0].index = 0;
511        canvas.receivers[1].index = 1;
512        // Card 0 now sits at x=8, card 1 at x=0; panel 0 still shows the
513        // canvas's left half, so it must arrive at screen x 8..16.
514        let mut wall = Wall::with_sink(Vec::new(), canvas, quick()).unwrap();
515        let frame = gradient(16, 4);
516        wall.show(&frame).unwrap();
517        let row0 = &wall.dev[1];
518        let px = row0[21..].as_chunks::<3>().0;
519        assert_eq!(px.len(), 16);
520        for x in 0..8u32 {
521            let [r, g, b] = frame.pixel(x, 0);
522            assert_eq!(px[8 + x as usize], [b, g, r], "canvas x {x} lands at screen x {}", 8 + x);
523        }
524    }
525
526    /// The old per-receiver loop, kept here so the single-card stream is
527    /// pinned byte for byte: for one card at the origin the receiver
528    /// framebuffer was the image itself, and every row went out with local
529    /// coordinates from 0.
530    fn old_single_card_stream(frame: &Frame, settings: Settings) -> Vec<u8> {
531        let mut stream = Vec::new();
532        let mut packet = Vec::new();
533        stream.extend_from_slice(&colorlight::brightness(settings.brightness));
534        for (y, row) in frame.rows().enumerate() {
535            for (i, chunk) in row.chunks(colorlight::MAX_PIXELS_PER_PACKET).enumerate() {
536                let offset = i * colorlight::MAX_PIXELS_PER_PACKET;
537                colorlight::pixel_row_into(&mut packet, y as u16, offset as u16, chunk, settings.color_order);
538                stream.extend_from_slice(&packet);
539            }
540        }
541        for _ in 0..settings.timing.latches {
542            stream.extend_from_slice(&colorlight::sync(settings.brightness));
543        }
544        stream
545    }
546
547    #[test]
548    fn a_single_card_at_the_origin_gets_the_same_bytes_as_before() {
549        for (w, h) in [(128, 64), (1000, 2)] {
550            let settings = Settings {
551                brightness: 40,
552                ..quick()
553            };
554            let frame = gradient(w, h);
555            let mut wall = Wall::with_sink(Vec::new(), Canvas::single(w, h), settings).unwrap();
556            wall.show(&frame).unwrap();
557            let new: Vec<u8> = wall.dev.concat();
558            assert_eq!(new, old_single_card_stream(&frame, settings), "{w}x{h}");
559        }
560    }
561
562    #[test]
563    fn oversized_walls_are_refused_before_anything_is_sent() {
564        let mut canvas = Canvas::single(8, 8);
565        canvas.receivers[0].width = 70_000;
566        let err = Wall::with_sink(Vec::new(), canvas, quick()).err().unwrap();
567        assert!(err.to_string().contains("receiver 0 at (0, 0) size 70000x8 extends past"), "{err}");
568
569        let huge = Canvas::single(70_000, 1);
570        let err = Wall::with_sink(Vec::new(), huge, quick()).err().unwrap();
571        assert!(err.to_string().contains("canvas is larger than 65535 px"), "{err}");
572
573        let mut bad = Canvas::single(8, 8);
574        bad.panels[0].x = 4;
575        let err = Wall::with_sink(Vec::new(), bad, quick()).err().unwrap();
576        assert!(err.to_string().starts_with("canvas is not valid:"), "{err}");
577    }
578
579    /// Counts frames and bytes; what the raw link would see, minus the syscall.
580    struct Counting {
581        packets: u64,
582        bytes: u64,
583    }
584
585    impl FrameSink for Counting {
586        fn send(&mut self, frame: &[u8]) -> Result<()> {
587            self.packets += 1;
588            self.bytes += frame.len() as u64;
589            Ok(())
590        }
591    }
592
593    /// Fifty cards (10 x 5 of 128x64, a 1280x320 screen), 300 frames:
594    /// microseconds per frame to render and to pack, and the packet count.
595    /// Run with `cargo test --release -p driver -- --ignored --nocapture`.
596    #[test]
597    #[ignore = "timing; run in release with --nocapture"]
598    fn fifty_cards_render_and_pack_time() {
599        const FRAMES: u32 = 300;
600        let canvas = Canvas::cards(128, 64, 10, 5);
601        let frame = gradient(1280, 320);
602        let sink = Counting { packets: 0, bytes: 0 };
603        let mut wall = Wall::with_sink(sink, canvas.clone(), quick()).unwrap();
604
605        let mut screen = canvas.screen_frame();
606        let t = Instant::now();
607        for _ in 0..FRAMES {
608            canvas.render_into(&frame, &mut screen);
609        }
610        let render_us = t.elapsed().as_secs_f64() * 1e6 / f64::from(FRAMES);
611        std::hint::black_box(&screen);
612
613        let t = Instant::now();
614        for _ in 0..FRAMES {
615            wall.show(&frame).unwrap();
616        }
617        let show_us = t.elapsed().as_secs_f64() * 1e6 / f64::from(FRAMES);
618
619        let packets = wall.dev.packets / u64::from(FRAMES);
620        let bytes = wall.dev.bytes / u64::from(FRAMES);
621        println!(
622            "50 cards, 1280x320: render {render_us:.0} us/frame, pack {:.0} us/frame, {packets} packets/frame ({} row packets), {bytes} bytes/frame",
623            show_us - render_us,
624            packets - 1 - u64::from(quick().timing.latches),
625        );
626    }
627}