1use core::convert::Infallible;
109
110use embedded_graphics::pixelcolor::RgbColor;
111use embedded_graphics::prelude::{DrawTarget, OriginDimensions, Point, Size};
112
113use super::{
114 map_index, Address, Entry, ADDR_TABLE, INTER_ROW_BLANK, LEAD_BLANK_DELAY, OE_ACTIVE, OE_BLANK,
115 TRAIL_BLANK_DELAY,
116};
117use crate::Color;
118use crate::{map_row_index, slot_addresses};
119use crate::{BcmLenReps, FrameBuffer, BCM_SEQUENCE_CAPACITY};
120use crate::{FrameBufferOperations, MutableFrameBuffer};
121
122#[derive(Clone, Copy, PartialEq, Debug)]
126#[repr(C)]
127pub struct PlaneRow<const COLS: usize> {
128 pub(crate) data: [Entry; COLS],
129 pub(crate) address: [Address; 4],
130}
131
132impl<const COLS: usize> PlaneRow<COLS> {
133 const fn new() -> Self {
134 Self {
135 data: [Entry::new(); COLS],
136 address: [Address::new(); 4],
137 }
138 }
139}
140
141const GAP_BYTES: usize = INTER_ROW_BLANK * core::mem::size_of::<Entry>();
144
145#[allow(clippy::absurd_extreme_comparisons)]
147const HAS_GAP: bool = GAP_BYTES > 0;
148
149const fn plane_suffix(plane_idx: usize, planes: usize) -> (usize, usize) {
158 let covered = if HAS_GAP && plane_idx == 0 {
159 1
160 } else {
161 planes - plane_idx
162 };
163 let reps = if plane_idx == 0 {
164 1
165 } else if HAS_GAP && plane_idx == 1 {
166 2
167 } else {
168 1 << (plane_idx - 1)
169 };
170 (covered, reps)
171}
172
173#[allow(clippy::absurd_extreme_comparisons)]
177const fn bcm_sequence<const COLS: usize, const PLANES: usize>(
178) -> [BcmLenReps; BCM_SEQUENCE_CAPACITY] {
179 let gap = HAS_GAP as usize;
180 let seq_len = PLANES + gap;
181 assert!(seq_len <= BCM_SEQUENCE_CAPACITY);
182 let plane_bytes = core::mem::size_of::<PlaneRow<COLS>>();
183 let mut seq = [BcmLenReps::ZERO; BCM_SEQUENCE_CAPACITY];
184 let mut within = 0usize;
185 while within < seq_len {
186 let entry = if within == 0 {
187 let (covered, reps) = plane_suffix(0, PLANES);
188 BcmLenReps {
189 len: plane_bytes * covered,
190 reps,
191 }
192 } else if HAS_GAP && within == 1 {
193 BcmLenReps {
194 len: GAP_BYTES,
195 reps: 1,
196 }
197 } else {
198 let (covered, reps) = plane_suffix(within - gap, PLANES);
199 BcmLenReps {
200 len: plane_bytes * covered,
201 reps,
202 }
203 };
204 seq[within] = entry;
205 within += 1;
206 }
207 seq
208}
209
210#[inline]
223#[allow(clippy::absurd_extreme_comparisons)]
224const fn make_row_plane_template<const COLS: usize>(
225 trail_blank: bool,
226 lead_blank: bool,
227) -> [Entry; COLS] {
228 let trailing = if lead_blank { LEAD_BLANK_DELAY } else { 0 };
229 let lead_start = COLS.saturating_sub(trailing);
230
231 let mut data = [Entry::new(); COLS];
232 let mut i = 0;
233 while i < COLS {
234 let blanked = (trail_blank && i < TRAIL_BLANK_DELAY) || i >= lead_start;
235 data[map_index(i)].0 = if blanked { OE_BLANK } else { OE_ACTIVE };
236 i += 1;
237 }
238 data
239}
240
241#[derive(Clone, Copy, PartialEq, Debug)]
251#[repr(C)]
252pub struct RowData<const COLS: usize, const PLANES: usize> {
253 pub(crate) planes: [PlaneRow<COLS>; PLANES],
254 pub(crate) gap: [Entry; INTER_ROW_BLANK],
255}
256
257impl<const COLS: usize, const PLANES: usize> RowData<COLS, PLANES> {
258 const fn new() -> Self {
259 Self {
260 planes: [PlaneRow::new(); PLANES],
261 gap: [Entry::new(); INTER_ROW_BLANK],
262 }
263 }
264
265 const fn format(&mut self, addr: u8) {
272 let src_addr = ADDR_TABLE[addr as usize];
273
274 let mut p = 0;
275 while p < PLANES {
276 let trail_blank = p == 1 || (p == 0 && PLANES == 1);
277 let lead_blank = p == 0;
278 let template = make_row_plane_template::<COLS>(trail_blank, lead_blank);
279 let mut c = 0;
280 while c < COLS {
281 self.planes[p].data[c] = template[c];
282 c += 1;
283 }
284 self.planes[p].address[0] = src_addr[0];
285 self.planes[p].address[1] = src_addr[1];
286 self.planes[p].address[2] = src_addr[2];
287 self.planes[p].address[3] = src_addr[3];
288 p += 1;
289 }
290
291 let mut i = 0;
292 #[allow(clippy::absurd_extreme_comparisons)]
294 while i < INTER_ROW_BLANK {
295 self.gap[i].0 = OE_BLANK;
296 i += 1;
297 }
298 }
299}
300
301#[derive(Copy, Clone)]
311#[repr(C, align(4))]
312pub struct DmaFrameBuffer<const NROWS: usize, const COLS: usize, const PLANES: usize> {
313 pub(crate) rows: [RowData<COLS, PLANES>; NROWS],
314}
315
316impl<const NROWS: usize, const COLS: usize, const PLANES: usize>
317 DmaFrameBuffer<NROWS, COLS, PLANES>
318{
319 #[must_use]
339 pub const fn new() -> Self {
340 assert!(NROWS >= 1 && NROWS <= 32, "NROWS must be within 1..=32");
341 assert!(PLANES >= 1 && PLANES <= 8, "PLANES must be within 1..=8");
342 #[cfg(feature = "esp32-ordering")]
343 assert!(
344 COLS % 4 == 0,
345 "esp32-ordering feature requires COLS to be divisible by 4"
346 );
347 let mut instance = Self {
348 rows: [RowData::new(); NROWS],
349 };
350 instance.format();
351 instance
352 }
353
354 #[must_use]
356 #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
357 pub const fn bcm_chunk_count() -> usize {
358 PLANES
359 }
360
361 #[must_use]
364 #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
365 pub const fn bcm_chunk_bytes() -> usize {
366 core::mem::size_of::<PlaneRow<COLS>>()
367 }
368
369 #[must_use]
371 pub const fn bcm_row_count() -> usize {
372 NROWS
373 }
374
375 #[must_use]
377 pub const fn bcm_row_bytes() -> usize {
378 GAP_BYTES
379 }
380
381 #[inline]
387 pub const fn format(&mut self) {
388 let mut slot = 0;
389 while slot < NROWS {
390 let (_, addr) = slot_addresses::<NROWS>(slot);
391 self.rows[slot].format(addr);
392 slot += 1;
393 }
394 }
395
396 #[inline]
398 pub fn erase(&mut self) {
399 const MASK: u8 = !crate::pinmap::LATCHED_COLOR_MASK;
400 for row in &mut self.rows {
401 for plane in &mut row.planes {
402 for entry in &mut plane.data {
403 entry.0 &= MASK;
404 }
405 }
406 }
407 }
408
409 #[inline]
411 pub fn set_pixel(&mut self, p: Point, color: Color) {
412 if p.x < 0 || p.y < 0 {
413 return;
414 }
415 self.set_pixel_internal(p.x as usize, p.y as usize, color);
416 }
417
418 #[inline]
419 fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
420 if x >= COLS || y >= NROWS * 2 {
421 return;
422 }
423
424 #[cfg(feature = "skip-black-pixels")]
427 if color == Color::BLACK {
428 return;
429 }
430
431 let row_idx = map_row_index::<NROWS>(if y < NROWS { y } else { y - NROWS });
432 let is_top = y < NROWS;
433 let red = color.r();
434 let green = color.g();
435 let blue = color.b();
436
437 for plane_idx in 0..PLANES {
438 let bit = (8 - PLANES + plane_idx) as u32;
439 let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
440 | ((u8::from(((green >> bit) & 1) != 0)) << 1)
441 | u8::from(((red >> bit) & 1) != 0);
442 let col_idx = map_index(x);
443 let entry = &mut self.rows[row_idx].planes[plane_idx].data[col_idx];
444 if is_top {
445 entry.set_color0_bits(bits);
446 } else {
447 entry.set_color1_bits(bits);
448 }
449 }
450 }
451
452 #[must_use]
458 pub fn pixel_row_ptr_len(&self, row_idx: usize, plane_idx: usize) -> (*const u8, usize) {
459 assert!(
460 row_idx < NROWS,
461 "row_idx {row_idx} out of range for {NROWS} rows"
462 );
463 assert!(
464 plane_idx < PLANES,
465 "plane_idx {plane_idx} out of range for {PLANES} planes"
466 );
467 let ptr = (&raw const self.rows[row_idx].planes[plane_idx]).cast::<u8>();
468 let len = core::mem::size_of::<PlaneRow<COLS>>();
469 (ptr, len)
470 }
471
472 #[must_use]
481 pub fn gap_ptr_len(&self, row_idx: usize) -> (*const u8, usize) {
482 assert!(
483 row_idx < NROWS,
484 "row_idx {row_idx} out of range for {NROWS} rows"
485 );
486 let ptr = (&raw const self.rows[row_idx].gap).cast::<u8>();
487 (ptr, GAP_BYTES)
488 }
489}
490
491impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
492 for DmaFrameBuffer<NROWS, COLS, PLANES>
493{
494 fn default() -> Self {
495 Self::new()
496 }
497}
498
499impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
500 for DmaFrameBuffer<NROWS, COLS, PLANES>
501{
502 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
503 f.debug_struct("DmaFrameBuffer")
504 .field("size", &core::mem::size_of_val(self))
505 .field("row_count", &NROWS)
506 .field("planes", &PLANES)
507 .field("cols", &COLS)
508 .finish()
509 }
510}
511
512#[cfg(feature = "defmt")]
513impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
514 for DmaFrameBuffer<NROWS, COLS, PLANES>
515{
516 fn format(&self, f: defmt::Formatter) {
517 defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
518 defmt::write!(f, " size: {}", core::mem::size_of_val(self));
519 }
520}
521
522impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
523 for DmaFrameBuffer<NROWS, COLS, PLANES>
524{
525 type Word = u8;
526
527 const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY] = bcm_sequence::<COLS, PLANES>();
528
529 const BCM_SEQUENCE_LEN: usize = PLANES + HAS_GAP as usize;
530
531 const BCM_SEQUENCE_COUNT: usize = NROWS;
532
533 const BCM_SEGMENTS_PER_GROUP: usize = PLANES + HAS_GAP as usize;
534
535 fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
536 let gap = usize::from(HAS_GAP);
537 let segments_per_row = PLANES + gap;
538 assert!(
539 index < NROWS * segments_per_row,
540 "segment index {index} out of range"
541 );
542 let row_idx = index / segments_per_row;
543 let within_row = index % segments_per_row;
544 if within_row == 0 {
545 let (ptr, _) = self.pixel_row_ptr_len(row_idx, 0);
546 ptr
547 } else if HAS_GAP && within_row == 1 {
548 let (ptr, _) = self.gap_ptr_len(row_idx);
549 ptr
550 } else {
551 let plane_idx = within_row - gap;
552 let (ptr, _) = self.pixel_row_ptr_len(row_idx, plane_idx);
553 ptr
554 }
555 }
556}
557
558impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
559 for DmaFrameBuffer<NROWS, COLS, PLANES>
560{
561 #[inline]
562 fn erase(&mut self) {
563 DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
564 }
565
566 #[inline]
567 fn set_pixel(&mut self, p: Point, color: Color) {
568 DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
569 }
570}
571
572impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
573 for DmaFrameBuffer<NROWS, COLS, PLANES>
574{
575}
576
577impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
578 for DmaFrameBuffer<NROWS, COLS, PLANES>
579{
580 fn size(&self) -> Size {
581 Size::new(COLS as u32, (NROWS * 2) as u32)
582 }
583}
584
585impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
586 for DmaFrameBuffer<NROWS, COLS, PLANES>
587{
588 type Color = Color;
589 type Error = Infallible;
590
591 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
592 where
593 I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
594 {
595 for pixel in pixels {
596 if pixel.0.x < 0 || pixel.0.y < 0 {
597 continue;
598 }
599 self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
600 }
601 Ok(())
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 extern crate std;
608
609 use super::super::{LEAD_BLANK_DELAY, TRAIL_BLANK_DELAY};
610 use super::*;
611 use embedded_graphics::prelude::*;
612 use std::format;
613
614 const TEST_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
615 128
616 } else {
617 64
618 };
619 const TEST_PLANES: usize = 8;
620 type TestBuffer = DmaFrameBuffer<16, TEST_COLS, TEST_PLANES>;
621
622 #[test]
623 fn format_sets_current_row_address_for_all_planes() {
624 let fb = TestBuffer::new();
625
626 for slot in 0..16 {
627 let (_, addr) = slot_addresses::<16>(slot);
628 for plane_idx in 0..TEST_PLANES {
629 let plane = &fb.rows[slot].planes[plane_idx];
630 assert_eq!(
631 plane.address[map_index(0)].addr(),
632 addr,
633 "slot {slot} plane {plane_idx} addr byte 0 should be the current row"
634 );
635 assert_eq!(
636 plane.address[map_index(1)].addr(),
637 addr,
638 "slot {slot} plane {plane_idx} addr byte 1 should be the current row"
639 );
640 assert_eq!(
641 plane.address[map_index(2)].addr(),
642 addr,
643 "slot {slot} plane {plane_idx} addr byte 2 should be the current row"
644 );
645 assert_eq!(
646 plane.address[map_index(3)].0,
647 OE_BLANK,
648 "slot {slot} plane {plane_idx} addr byte 3 should be blanked with addr 0"
649 );
650 }
651 }
652 }
653
654 #[test]
655 fn address_bytes_have_two_latch_pulses() {
656 let fb = TestBuffer::new();
657
658 for row in &fb.rows {
659 for plane in &row.planes {
660 let latch_count = plane.address.iter().filter(|a| a.latch()).count();
661 assert_eq!(latch_count, 2, "expected exactly two latch pulses");
662 assert!(plane.address[map_index(0)].latch());
663 assert!(plane.address[map_index(1)].latch());
664 }
665 }
666 }
667
668 #[test]
669 fn planes_match_row_plane_template() {
670 let fb = TestBuffer::new();
671
672 for row_idx in 0..16 {
673 for p in 0..TEST_PLANES {
674 let expected = make_row_plane_template::<TEST_COLS>(p == 1, p == 0);
675 assert_eq!(
676 fb.rows[row_idx].planes[p].data, expected,
677 "row {row_idx} plane {p} doesn't match template"
678 );
679 }
680 }
681 }
682
683 #[test]
684 fn first_plane_has_lead_blank_only() {
685 let fb = TestBuffer::new();
686 let oe_active = !cfg!(feature = "invert-oe");
687
688 for row in &fb.rows {
689 let plane = &row.planes[0];
690
691 let first_idx = map_index(0);
695 assert_eq!(
696 plane.data[first_idx].output_enable(),
697 oe_active,
698 "plane 0: first pixel should have OE active (no trail blank)"
699 );
700
701 for i in (TEST_COLS - LEAD_BLANK_DELAY)..TEST_COLS {
703 assert_eq!(
704 plane.data[map_index(i)].output_enable(),
705 !oe_active,
706 "plane 0: lead blank entry {i} should have OE blank"
707 );
708 }
709 let active_idx = map_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
710 assert_eq!(
711 plane.data[active_idx].output_enable(),
712 oe_active,
713 "plane 0: entry before the lead blank should have OE active"
714 );
715 }
716 }
717
718 #[test]
719 fn second_plane_has_trail_blank_only() {
720 let fb = TestBuffer::new();
721 let oe_active = !cfg!(feature = "invert-oe");
722
723 for row in &fb.rows {
724 let plane = &row.planes[1];
725
726 for i in 0..TRAIL_BLANK_DELAY {
728 assert_eq!(
729 plane.data[map_index(i)].output_enable(),
730 !oe_active,
731 "plane 1: trail blank entry {i} should have OE blank"
732 );
733 }
734 let active_idx = map_index(TRAIL_BLANK_DELAY);
735 assert_eq!(
736 plane.data[active_idx].output_enable(),
737 oe_active,
738 "plane 1: entry after the trail blank should have OE active"
739 );
740
741 let last_idx = map_index(TEST_COLS - 1);
744 assert_eq!(
745 plane.data[last_idx].output_enable(),
746 oe_active,
747 "plane 1: last pixel should have OE active (no lead blank)"
748 );
749 }
750 }
751
752 #[test]
753 fn middle_planes_have_full_width_window() {
754 let fb = TestBuffer::new();
755 let oe_active = !cfg!(feature = "invert-oe");
756
757 for row in &fb.rows {
760 for plane in row.planes.iter().take(TEST_PLANES - 1).skip(2) {
761 let first_idx = map_index(0);
762 assert_eq!(
763 plane.data[first_idx].output_enable(),
764 oe_active,
765 "middle plane: first pixel should have OE active"
766 );
767 let last_idx = map_index(TEST_COLS - 1);
768 assert_eq!(
769 plane.data[last_idx].output_enable(),
770 oe_active,
771 "middle plane: last pixel should have OE active"
772 );
773 }
774 }
775 }
776
777 #[test]
778 fn last_plane_has_full_width_window() {
779 let fb = TestBuffer::new();
780 let oe_active = !cfg!(feature = "invert-oe");
781 let last = TEST_PLANES - 1;
782
783 for row in &fb.rows {
784 let plane = &row.planes[last];
785
786 let last_idx = map_index(TEST_COLS - 1);
790 assert_eq!(
791 plane.data[last_idx].output_enable(),
792 oe_active,
793 "last plane: last pixel should have OE active (no lead blank)"
794 );
795
796 let first_idx = map_index(0);
798 assert_eq!(
799 plane.data[first_idx].output_enable(),
800 oe_active,
801 "last plane: first pixel should have OE active (no trail blank)"
802 );
803 }
804 }
805
806 #[test]
807 fn oe_window_sizes_match_template() {
808 let fb = TestBuffer::new();
809 let oe_active = !cfg!(feature = "invert-oe");
810
811 for row in &fb.rows {
812 for (p, plane) in row.planes.iter().enumerate() {
813 let trail = if p == 1 { TRAIL_BLANK_DELAY } else { 0 };
814 let lead = if p == 0 { LEAD_BLANK_DELAY } else { 0 };
815 let expected_active = TEST_COLS.saturating_sub(trail + lead);
816 let active = plane
817 .data
818 .iter()
819 .filter(|e| e.output_enable() == oe_active)
820 .count();
821 assert_eq!(active, expected_active, "plane {p} active window size");
822 }
823 }
824 }
825
826 #[test]
827 fn single_plane_has_trail_and_lead_blank() {
828 type OnePlane = DmaFrameBuffer<16, TEST_COLS, 1>;
829 let fb = OnePlane::new();
830
831 for slot in 0..16 {
832 let (_, addr) = slot_addresses::<16>(slot);
833 let plane = &fb.rows[slot].planes[0];
834 assert_eq!(
835 plane.address[map_index(0)].addr(),
836 addr,
837 "single plane slot {slot}: should use the current row address"
838 );
839
840 let expected = make_row_plane_template::<TEST_COLS>(true, true);
843 assert_eq!(
844 plane.data, expected,
845 "single plane slot {slot}: template mismatch"
846 );
847 }
848 }
849
850 #[test]
851 fn set_pixel_maps_top_half_bits_per_plane() {
852 let mut fb = TestBuffer::new();
853 let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
854 fb.set_pixel(Point::new(2, 3), color);
855
856 for plane_idx in 0..TEST_PLANES {
857 let bit = 8 - TEST_PLANES + plane_idx;
858 let entry = fb.rows[map_row_index::<16>(3)].planes[plane_idx].data[map_index(2)];
859 assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
860 assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
861 assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
862 }
863 }
864
865 #[test]
866 fn set_pixel_maps_bottom_half_bits_per_plane() {
867 let mut fb = TestBuffer::new();
868 let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
869 fb.set_pixel(Point::new(4, 20), color);
870
871 for plane_idx in 0..TEST_PLANES {
872 let bit = 8 - TEST_PLANES + plane_idx;
873 let entry = fb.rows[map_row_index::<16>(4)].planes[plane_idx].data[map_index(4)];
874 assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
875 assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
876 assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
877 }
878 }
879
880 #[test]
881 #[cfg(feature = "reverse-row-order")]
882 fn reverse_row_order_stores_rows_back_to_front() {
883 let mut fb = TestBuffer::new();
884
885 assert_eq!(fb.rows[0].planes[0].address[map_index(0)].addr(), 15);
889 assert_eq!(fb.rows[15].planes[0].address[map_index(0)].addr(), 0);
890
891 fb.set_pixel(Point::new(2, 0), Color::RED);
893 let col2 = map_index(2);
894 assert!(fb.rows[15].planes[TEST_PLANES - 1].data[col2].red1());
895 assert!(!fb.rows[0].planes[TEST_PLANES - 1].data[col2].red1());
896 }
897
898 #[test]
899 fn erase_clears_only_color_bits() {
900 let mut fb = TestBuffer::new();
901 let oe_before =
902 fb.rows[map_row_index::<16>(0)].planes[0].data[map_index(1)].output_enable();
903 let addr_before = fb.rows[map_row_index::<16>(3)].planes[2].address;
904 fb.set_pixel(Point::new(0, 0), Color::WHITE);
905 fb.erase();
906
907 for row in &fb.rows {
908 for plane in &row.planes {
909 for entry in &plane.data {
910 assert!(!entry.red1());
911 assert!(!entry.grn1());
912 assert!(!entry.blu1());
913 assert!(!entry.red2());
914 assert!(!entry.grn2());
915 assert!(!entry.blu2());
916 }
917 }
918 }
919
920 assert_eq!(
921 fb.rows[map_row_index::<16>(0)].planes[0].data[map_index(1)].output_enable(),
922 oe_before
923 );
924 assert_eq!(
925 fb.rows[map_row_index::<16>(3)].planes[2].address,
926 addr_before
927 );
928 }
929
930 #[test]
931 fn draw_target_iter_sets_pixels() {
932 let mut fb = TestBuffer::new();
933 let pixels = [Pixel(Point::new(1, 1), Color::RED)];
934 let result = fb.draw_iter(pixels);
935 assert!(result.is_ok());
936
937 for plane_idx in 0..TEST_PLANES {
938 let bit = 8 - TEST_PLANES + plane_idx;
939 let entry = fb.rows[map_row_index::<16>(1)].planes[plane_idx].data[map_index(1)];
940 assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
941 assert!(!entry.grn1());
942 assert!(!entry.blu1());
943 }
944 }
945
946 #[test]
947 fn set_pixel_ignores_out_of_bounds_and_negative() {
948 let mut fb = TestBuffer::new();
949 let before = fb.rows;
950 fb.set_pixel(Point::new(-1, 0), Color::WHITE);
951 fb.set_pixel(Point::new(0, -1), Color::WHITE);
952 fb.set_pixel(Point::new(TEST_COLS as i32, 0), Color::WHITE);
953 fb.set_pixel(Point::new(0, 32), Color::WHITE);
954 assert_eq!(fb.rows, before);
955 }
956
957 #[test]
958 #[cfg(feature = "skip-black-pixels")]
959 fn test_skip_black_pixels_enabled() {
960 let mut fb = TestBuffer::new();
961
962 fb.set_pixel_internal(10, 5, Color::RED);
964
965 let mapped_col_10 = map_index(10);
967 assert!(fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
968 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
969 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
970
971 fb.set_pixel_internal(10, 5, Color::BLACK);
973
974 assert!(fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
976 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
977 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
978 }
979
980 #[test]
981 #[cfg(not(feature = "skip-black-pixels"))]
982 fn test_skip_black_pixels_disabled() {
983 let mut fb = TestBuffer::new();
984
985 fb.set_pixel_internal(10, 5, Color::RED);
987
988 let mapped_col_10 = map_index(10);
990 assert!(fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
991 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
992 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
993
994 fb.set_pixel_internal(10, 5, Color::BLACK);
996
997 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
999 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
1000 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
1001 }
1002
1003 #[test]
1004 #[should_panic(expected = "NROWS must be within 1..=32")]
1005 fn new_panics_for_too_many_rows() {
1006 let _ = DmaFrameBuffer::<33, TEST_COLS, 8>::new();
1007 }
1008
1009 #[test]
1010 #[should_panic(expected = "PLANES must be within 1..=8")]
1011 fn new_panics_for_too_many_planes() {
1012 let _ = DmaFrameBuffer::<16, TEST_COLS, 9>::new();
1013 }
1014
1015 #[test]
1016 fn pixel_row_ptr_len_returns_correct_pointers() {
1017 let fb = TestBuffer::new();
1018 let (ptr, len) = fb.pixel_row_ptr_len(0, 0);
1019 assert_eq!(len, core::mem::size_of::<PlaneRow<TEST_COLS>>());
1020 assert_eq!(ptr, (&raw const fb.rows[0].planes[0]).cast::<u8>());
1021 }
1022
1023 #[test]
1024 fn gap_ptr_len_returns_correct_pointers() {
1025 let fb = TestBuffer::new();
1026 let (ptr, len) = fb.gap_ptr_len(0);
1027 assert_eq!(len, GAP_BYTES);
1028 assert_eq!(ptr, (&raw const fb.rows[0].gap).cast::<u8>());
1029 }
1030
1031 #[test]
1032 #[should_panic(expected = "out of range")]
1033 fn pixel_row_ptr_len_panics_for_invalid_row() {
1034 let fb = TestBuffer::new();
1035 let _ = fb.pixel_row_ptr_len(16, 0);
1036 }
1037
1038 #[test]
1039 #[should_panic(expected = "out of range")]
1040 fn pixel_row_ptr_len_panics_for_invalid_plane() {
1041 let fb = TestBuffer::new();
1042 let _ = fb.pixel_row_ptr_len(0, TEST_PLANES);
1043 }
1044
1045 #[test]
1046 #[should_panic(expected = "out of range")]
1047 fn gap_ptr_len_panics_for_invalid_row() {
1048 let fb = TestBuffer::new();
1049 let _ = fb.gap_ptr_len(16);
1050 }
1051
1052 #[test]
1053 fn origin_dimensions_match_panel_geometry() {
1054 let fb = TestBuffer::new();
1055 assert_eq!(fb.size(), Size::new(TEST_COLS as u32, 32));
1056 }
1057
1058 #[test]
1059 fn default_matches_new() {
1060 let fb_default = TestBuffer::default();
1061 let fb_new = TestBuffer::new();
1062 assert_eq!(fb_default.rows, fb_new.rows);
1063 }
1064
1065 #[test]
1066 fn debug_impl_includes_shape_information() {
1067 let fb = TestBuffer::new();
1068 let s = format!("{fb:?}");
1069 assert!(s.contains("DmaFrameBuffer"));
1070 assert!(s.contains("row_count"));
1071 assert!(s.contains("planes"));
1072 }
1073
1074 static STATIC_FB: TestBuffer = TestBuffer::new();
1075
1076 #[test]
1077 fn static_construction_is_formatted() {
1078 let runtime_fb = TestBuffer::new();
1079
1080 for (ri, row) in STATIC_FB.rows.iter().enumerate() {
1081 assert_eq!(
1082 row.planes, runtime_fb.rows[ri].planes,
1083 "static vs runtime plane mismatch at row {ri}"
1084 );
1085 assert_eq!(
1086 row.gap, runtime_fb.rows[ri].gap,
1087 "static vs runtime gap mismatch at row {ri}"
1088 );
1089 }
1090 }
1091
1092 #[test]
1093 fn format_reinitializes_at_runtime() {
1094 let mut fb = TestBuffer::new();
1095 fb.erase();
1096 fb.format();
1097
1098 for (ri, row) in fb.rows.iter().enumerate() {
1099 assert_eq!(
1100 row.planes, STATIC_FB.rows[ri].planes,
1101 "re-formatted vs static plane mismatch at row {ri}"
1102 );
1103 assert_eq!(
1104 row.gap, STATIC_FB.rows[ri].gap,
1105 "re-formatted vs static gap mismatch at row {ri}"
1106 );
1107 }
1108 }
1109
1110 #[test]
1111 fn framebuffer_operations_trait_delegates_correctly() {
1112 let mut fb = TestBuffer::new();
1113 FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
1114
1115 assert!(fb.rows[map_row_index::<16>(5)].planes[TEST_PLANES - 1].data[map_index(3)].grn1());
1117
1118 FrameBufferOperations::erase(&mut fb);
1119 for row in &fb.rows {
1120 for plane in &row.planes {
1121 for entry in &plane.data {
1122 assert!(!entry.red1());
1123 assert!(!entry.grn1());
1124 assert!(!entry.blu1());
1125 assert!(!entry.red2());
1126 assert!(!entry.grn2());
1127 assert!(!entry.blu2());
1128 }
1129 }
1130 }
1131 }
1132
1133 #[test]
1134 fn row_data_size_matches_expected_layout() {
1135 let expected_planes = core::mem::size_of::<PlaneRow<TEST_COLS>>() * TEST_PLANES;
1136 assert_eq!(
1137 core::mem::size_of::<RowData<TEST_COLS, TEST_PLANES>>(),
1138 expected_planes + GAP_BYTES
1139 );
1140 }
1141
1142 #[test]
1143 fn bcm_segment_count_equals_rows_times_planes_plus_gap() {
1144 let fb = TestBuffer::new();
1145 let gap = if HAS_GAP { 1 } else { 0 };
1146 assert_eq!(fb.bcm_segment_count(), 16 * (TEST_PLANES + gap));
1147 }
1148
1149 #[test]
1150 fn bcm_segments_per_group_equals_planes_plus_gap() {
1151 let fb = TestBuffer::new();
1152 let gap = if HAS_GAP { 1 } else { 0 };
1153 assert_eq!(fb.bcm_segments_per_group(), TEST_PLANES + gap);
1154 assert_eq!(
1155 fb.bcm_segment_count() % fb.bcm_segments_per_group(),
1156 0,
1157 "segment count must be divisible by segments_per_group"
1158 );
1159 }
1160
1161 #[test]
1162 fn bcm_segments_interleave_pixel_and_gap() {
1163 let fb = TestBuffer::new();
1164 let gap = if HAS_GAP { 1 } else { 0 };
1165 let segments_per_row = TEST_PLANES + gap;
1166
1167 for row in 0..16usize {
1168 let seg = fb.bcm_segment(row * segments_per_row);
1170 let (ptr, plane_len) = fb.pixel_row_ptr_len(row, 0);
1171 let covered0 = if HAS_GAP { 1 } else { TEST_PLANES };
1172 assert_eq!(seg.ptr, ptr, "wrong ptr at row {row} plane 0");
1173 assert_eq!(
1174 seg.len,
1175 plane_len * covered0,
1176 "wrong len at row {row} plane 0"
1177 );
1178 assert_eq!(seg.reps, 1, "wrong reps at row {row} plane 0");
1179
1180 if HAS_GAP {
1182 let seg = fb.bcm_segment(row * segments_per_row + 1);
1183 let (ptr, len) = fb.gap_ptr_len(row);
1184 assert_eq!(seg.ptr, ptr, "wrong gap ptr at row {row}");
1185 assert_eq!(seg.len, len, "wrong gap len at row {row}");
1186 assert_eq!(seg.reps, 1, "gap reps must be 1 at row {row}");
1187 }
1188
1189 for plane in 1..TEST_PLANES {
1191 let idx = row * segments_per_row + plane + gap;
1192 let seg = fb.bcm_segment(idx);
1193 let (ptr, plane_len) = fb.pixel_row_ptr_len(row, plane);
1194 let expected_reps = if HAS_GAP && plane == 1 {
1195 2
1196 } else {
1197 1 << (plane - 1)
1198 };
1199 assert_eq!(seg.ptr, ptr, "wrong ptr at row {row} plane {plane}");
1200 assert_eq!(
1201 seg.len,
1202 plane_len * (TEST_PLANES - plane),
1203 "wrong len at row {row} plane {plane}"
1204 );
1205 assert_eq!(
1206 seg.reps, expected_reps,
1207 "wrong reps at row {row} plane {plane}"
1208 );
1209 }
1210 }
1211 }
1212
1213 #[test]
1214 fn bcm_segment_total_reps_per_row() {
1215 let fb = TestBuffer::new();
1216 let gap = if HAS_GAP { 1 } else { 0 };
1217 let segments_per_row = TEST_PLANES + gap;
1218
1219 for row in 0..16usize {
1220 let mut pixel_reps = 0;
1224 for plane in 0..TEST_PLANES {
1225 let offset = if plane == 0 { 0 } else { plane + gap };
1227 pixel_reps += fb.bcm_segment(row * segments_per_row + offset).reps;
1228 }
1229 let expected = if HAS_GAP {
1230 (1 << (TEST_PLANES - 1)) + 1
1231 } else {
1232 1 << (TEST_PLANES - 1)
1233 };
1234 assert_eq!(pixel_reps, expected, "total pixel reps wrong for row {row}");
1235 }
1236 }
1237
1238 #[test]
1239 fn bcm_segment_plane_coverage_matches_bcm_weights() {
1240 let fb = TestBuffer::new();
1241 let gap = if HAS_GAP { 1 } else { 0 };
1242 let segments_per_row = TEST_PLANES + gap;
1243 let plane_bytes = core::mem::size_of::<PlaneRow<TEST_COLS>>();
1244
1245 for row in 0..16usize {
1246 for plane in 0..TEST_PLANES {
1247 let mut coverage = 0;
1251 for first in 0..=plane {
1252 let offset = if first == 0 { 0 } else { first + gap };
1254 let seg = fb.bcm_segment(row * segments_per_row + offset);
1255 if first + seg.len / plane_bytes > plane {
1256 coverage += seg.reps;
1257 }
1258 }
1259 assert_eq!(
1260 coverage,
1261 1 << plane,
1262 "plane {plane} coverage wrong for row {row}"
1263 );
1264 }
1265 }
1266 }
1267
1268 #[test]
1269 #[should_panic(expected = "out of range")]
1270 fn bcm_segment_panics_for_invalid_index() {
1271 let fb = TestBuffer::new();
1272 let gap = if HAS_GAP { 1 } else { 0 };
1273 let segments_per_row = TEST_PLANES + gap;
1274 let _ = fb.bcm_segment(16 * segments_per_row);
1275 }
1276
1277 #[test]
1278 fn bcm_sequence_matches_runtime_segments() {
1279 use crate::FrameBuffer;
1280 let fb = TestBuffer::new();
1281 assert_eq!(
1282 TestBuffer::BCM_SEGMENT_COUNT,
1283 TestBuffer::BCM_SEQUENCE_LEN * TestBuffer::BCM_SEQUENCE_COUNT
1284 );
1285 assert_eq!(fb.bcm_segment_count(), TestBuffer::BCM_SEGMENT_COUNT);
1286 assert_eq!(
1287 fb.bcm_segments_per_group(),
1288 TestBuffer::BCM_SEGMENTS_PER_GROUP
1289 );
1290 assert_eq!(
1291 TestBuffer::BCM_SEQUENCE_LEN % TestBuffer::BCM_SEGMENTS_PER_GROUP,
1292 0
1293 );
1294 for i in 0..TestBuffer::BCM_SEGMENT_COUNT {
1295 let entry = TestBuffer::BCM_SEQUENCE[i % TestBuffer::BCM_SEQUENCE_LEN];
1296 let seg = fb.bcm_segment(i);
1297 assert_eq!((seg.len, seg.reps), (entry.len, entry.reps), "segment {i}");
1298 assert!(!seg.ptr.is_null(), "segment {i} has null pointer");
1299 }
1300 for entry in &TestBuffer::BCM_SEQUENCE[TestBuffer::BCM_SEQUENCE_LEN..] {
1301 assert_eq!(*entry, crate::BcmLenReps::ZERO, "padding must be zero");
1302 }
1303 }
1304
1305 #[test]
1306 fn bcm_segment_data_contains_written_pixels() {
1307 const TRINITY_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
1308 128
1309 } else {
1310 64
1311 };
1312 type TrinityBuf = DmaFrameBuffer<32, TRINITY_COLS, 5>;
1313 let mut fb = TrinityBuf::new();
1314
1315 fb.set_pixel(Point::new(10, 0), Color::WHITE);
1316
1317 let col_idx = map_index(10);
1318 let gap = usize::from(HAS_GAP);
1319 let row0_base = map_row_index::<32>(0) * (5 + gap);
1321
1322 for plane_idx in 0..5usize {
1323 let seg = fb.bcm_segment(if plane_idx == 0 {
1326 row0_base
1327 } else {
1328 row0_base + plane_idx + gap
1329 });
1330
1331 let data = unsafe { core::slice::from_raw_parts(seg.ptr, TRINITY_COLS) };
1332 let raw = data[col_idx];
1333 let entry = Entry(raw);
1334
1335 let bit = 8 - 5 + plane_idx; let expect_r = (0xFF >> bit) & 1 != 0;
1337 let expect_g = (0xFF >> bit) & 1 != 0;
1338 let expect_b = (0xFF >> bit) & 1 != 0;
1339 assert_eq!(
1340 entry.red1(),
1341 expect_r,
1342 "plane {plane_idx} (bit {bit}) R1 mismatch: raw={raw:#04x}"
1343 );
1344 assert_eq!(
1345 entry.grn1(),
1346 expect_g,
1347 "plane {plane_idx} (bit {bit}) G1 mismatch: raw={raw:#04x}"
1348 );
1349 assert_eq!(
1350 entry.blu1(),
1351 expect_b,
1352 "plane {plane_idx} (bit {bit}) B1 mismatch: raw={raw:#04x}"
1353 );
1354 }
1355 }
1356
1357 #[test]
1358 fn bcm_segment_data_contains_gradient_pixels() {
1359 const TRINITY_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
1360 128
1361 } else {
1362 64
1363 };
1364 type TrinityBuf = DmaFrameBuffer<32, TRINITY_COLS, 5>;
1365 let mut fb = TrinityBuf::new();
1366
1367 let step: u8 = (256 / TRINITY_COLS) as u8;
1368 for x in 0..TRINITY_COLS {
1369 let brightness = (x as u8).wrapping_mul(step);
1370 fb.set_pixel(Point::new(x as i32, 0), Color::new(brightness, 0, 0));
1371 }
1372
1373 let mut any_nonzero = false;
1374 let gap = usize::from(HAS_GAP);
1375 let row0_base = map_row_index::<32>(0) * (5 + gap);
1377 for plane_idx in 0..5usize {
1378 let seg = fb.bcm_segment(if plane_idx == 0 {
1381 row0_base
1382 } else {
1383 row0_base + plane_idx + gap
1384 });
1385 let data = unsafe { core::slice::from_raw_parts(seg.ptr, TRINITY_COLS) };
1386
1387 for x in 0..TRINITY_COLS {
1388 let col_idx = map_index(x);
1389 let entry = Entry(data[col_idx]);
1390 let brightness = (x as u8).wrapping_mul(step);
1391 let bit = 8 - 5 + plane_idx; let expect_r = (brightness >> bit) & 1 != 0;
1393 assert_eq!(
1394 entry.red1(),
1395 expect_r,
1396 "x={x} plane={plane_idx} bit={bit} brightness={brightness} \
1397 raw={:#04x}",
1398 data[col_idx]
1399 );
1400 if expect_r {
1401 any_nonzero = true;
1402 }
1403 }
1404 }
1405 assert!(
1406 any_nonzero,
1407 "gradient should produce at least some non-zero color bits"
1408 );
1409 }
1410}