1use anyhow::{Context, Result};
8use wall::{Canvas, Frame};
9use rawlink::Link;
10use std::ops::Range;
11use std::time::{Duration, Instant};
12
13const RECV_TIMEOUT: Duration = Duration::from_millis(200);
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct Timing {
20 pub latches: u32,
23 pub latch_gap: Duration,
26 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct Settings {
44 pub brightness: u8,
45 pub color_order: colorlight::ColorOrder,
46 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
64pub trait FrameSink {
67 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
80pub struct Wall<S: FrameSink = Link> {
82 dev: S,
83 canvas: Canvas,
84 settings: Settings,
85 announced: bool,
86 frames_sent: u64,
87 screen: Frame,
89 packet: Vec<u8>,
91 brightness_frame: [u8; 77],
92 sync_frame: [u8; 112],
93}
94
95impl Wall<Link> {
96 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 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 pub fn set_brightness(&mut self, brightness: u8) {
147 self.set_gains(brightness, [brightness; 3]);
148 }
149
150 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 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 pub fn show(&mut self, frame: &Frame) -> Result<()> {
194 let all = 0..self.canvas.height;
195 self.show_rows(frame, all)
196 }
197
198 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 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
253pub 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 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 self.next = now;
284 }
285 }
286
287 #[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 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 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 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 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 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 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 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 #[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}