1use core::convert::Infallible;
115
116use embedded_graphics::pixelcolor::RgbColor;
117use embedded_graphics::prelude::{DrawTarget, OriginDimensions, Point, Size};
118
119use crate::Color;
120use crate::{map_row_index, slot_addresses};
121use crate::{BcmLenReps, FrameBuffer, BCM_SEQUENCE_CAPACITY};
122use crate::{FrameBufferOperations, MutableFrameBuffer};
123
124use super::{
125 map_index, Entry, INTER_ROW_BLANK, LEAD_BLANK_DELAY, OE_ACTIVE, OE_BLANK, TRAIL_BLANK_DELAY,
126};
127
128#[derive(Clone, Copy, PartialEq, Debug)]
130#[repr(C)]
131pub struct PixelPlane<const COLS: usize> {
132 pub(crate) data: [Entry; COLS],
133}
134
135impl<const COLS: usize> PixelPlane<COLS> {
136 const fn new() -> Self {
137 Self {
138 data: [Entry::new(); COLS],
139 }
140 }
141}
142
143const GAP_BYTES: usize = INTER_ROW_BLANK * core::mem::size_of::<Entry>();
146
147const TRAILER_BYTES: usize = {
150 #[cfg(feature = "tail-closes-latch")]
151 let tail = core::mem::size_of::<Entry>() * 2;
152 #[cfg(not(feature = "tail-closes-latch"))]
153 let tail = 0;
154
155 tail
156};
157
158const fn plane_suffix(plane_idx: usize, planes: usize) -> (usize, usize) {
167 let covered = if cfg!(feature = "inter-row-blank") && plane_idx == 0 {
168 1
169 } else {
170 planes - plane_idx
171 };
172 let reps = if plane_idx == 0 {
173 1
174 } else if cfg!(feature = "inter-row-blank") && plane_idx == 1 {
175 2
176 } else {
177 1 << (plane_idx - 1)
178 };
179 (covered, reps)
180}
181
182#[allow(clippy::absurd_extreme_comparisons)]
187const fn bcm_sequence<const COLS: usize, const PLANES: usize>(
188) -> [BcmLenReps; BCM_SEQUENCE_CAPACITY] {
189 let seq_len = PLANES;
190 assert!(seq_len <= BCM_SEQUENCE_CAPACITY);
191 let plane_bytes = COLS * core::mem::size_of::<Entry>();
192 let mut seq = [BcmLenReps::ZERO; BCM_SEQUENCE_CAPACITY];
193 let mut within = 0usize;
194 while within < seq_len {
195 let entry = if within == 0 {
196 let len = if cfg!(feature = "inter-row-blank") {
200 plane_bytes + GAP_BYTES
201 } else {
202 plane_bytes * PLANES + TRAILER_BYTES
203 };
204 BcmLenReps { len, reps: 1 }
205 } else {
206 let (covered, reps) = plane_suffix(within, PLANES);
207 BcmLenReps {
208 len: plane_bytes * covered + TRAILER_BYTES,
209 reps,
210 }
211 };
212 seq[within] = entry;
213 within += 1;
214 }
215 seq
216}
217
218#[allow(clippy::absurd_extreme_comparisons)]
227#[inline]
228const fn make_row_plane_template<const COLS: usize>(
229 addr: u8,
230 trail_blank: bool,
231 lead_blank: bool,
232) -> [Entry; COLS] {
233 let mut data = [Entry::new(); COLS];
234 let mut i = 0;
235
236 while i < COLS {
237 let is_last = i == COLS - 1;
238
239 let blanked = is_last
240 || (trail_blank && i < TRAIL_BLANK_DELAY)
241 || (lead_blank && i >= COLS.saturating_sub(LEAD_BLANK_DELAY + 1));
242
243 let oe = if blanked { OE_BLANK } else { OE_ACTIVE };
244 let mut val = addr as u16 | oe;
245 if is_last {
246 val |= 0b0010_0000; }
248
249 data[map_index(i)] = Entry::from_raw(val);
250 i += 1;
251 }
252
253 data
254}
255
256#[derive(Clone, Copy, PartialEq, Debug)]
270#[repr(C)]
271pub struct RowData<const COLS: usize, const PLANES: usize> {
272 #[cfg(feature = "inter-row-blank")]
273 pub(crate) plane0: PixelPlane<COLS>,
274 #[cfg(feature = "inter-row-blank")]
275 pub(crate) gap: [Entry; INTER_ROW_BLANK],
276 pub(crate) planes: [PixelPlane<COLS>; PLANES],
277 #[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
278 pub(crate) padding: Entry,
279 #[cfg(feature = "tail-closes-latch")]
280 pub(crate) tail: Entry,
281 #[cfg(all(not(feature = "esp32-ordering"), feature = "tail-closes-latch"))]
282 pub(crate) padding: Entry,
283}
284
285impl<const COLS: usize, const PLANES: usize> RowData<COLS, PLANES> {
286 const fn new() -> Self {
287 Self {
288 #[cfg(feature = "inter-row-blank")]
289 plane0: PixelPlane::new(),
290 #[cfg(feature = "inter-row-blank")]
291 gap: [Entry::new(); INTER_ROW_BLANK],
292 planes: [PixelPlane::new(); PLANES],
293 #[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
294 padding: Entry::new(),
295 #[cfg(feature = "tail-closes-latch")]
296 tail: Entry::new(),
297 #[cfg(all(not(feature = "esp32-ordering"), feature = "tail-closes-latch"))]
298 padding: Entry::new(),
299 }
300 }
301
302 #[cfg(test)]
304 #[inline]
305 const fn plane(&self, p: usize) -> &PixelPlane<COLS> {
306 &self.planes[p]
307 }
308
309 #[inline]
311 const fn plane_mut(&mut self, p: usize) -> &mut PixelPlane<COLS> {
312 &mut self.planes[p]
313 }
314
315 #[cfg(feature = "inter-row-blank")]
318 #[inline]
319 const fn sync_plane0(&mut self) {
320 self.plane0 = self.planes[0];
321 }
322
323 const fn format(&mut self, prev_addr: u8, addr: u8) {
328 let mut p = 0;
329 while p < PLANES {
330 let is_first = p == 0;
331
332 if is_first {
333 let template = make_row_plane_template::<COLS>(prev_addr, PLANES == 1, true);
338 let mut c = 0;
339 while c < COLS {
340 self.plane_mut(p).data[c] = template[c];
341 c += 1;
342 }
343 } else {
344 let trail = p == 1;
347 let template = make_row_plane_template::<COLS>(addr, trail, false);
348 let mut c = 0;
349 while c < COLS {
350 self.plane_mut(p).data[c] = template[c];
351 c += 1;
352 }
353 }
354
355 p += 1;
356 }
357
358 #[cfg(feature = "inter-row-blank")]
361 {
362 self.sync_plane0();
363 }
364
365 #[cfg(feature = "inter-row-blank")]
372 {
373 let gap_entry = Entry::from_raw(prev_addr as u16 | OE_BLANK);
374 let mut i = 0;
375 while i < INTER_ROW_BLANK {
376 self.gap[i] = gap_entry;
377 i += 1;
378 }
379 }
380
381 #[cfg(feature = "tail-closes-latch")]
382 {
383 self.tail = Entry::from_raw(addr as u16 | OE_BLANK);
384 self.padding = Entry::from_raw(addr as u16 | OE_BLANK);
385 }
386 }
387}
388
389#[derive(Copy, Clone)]
399#[repr(C, align(4))]
400pub struct DmaFrameBuffer<const NROWS: usize, const COLS: usize, const PLANES: usize> {
401 pub(crate) rows: [RowData<COLS, PLANES>; NROWS],
402}
403
404impl<const NROWS: usize, const COLS: usize, const PLANES: usize>
405 DmaFrameBuffer<NROWS, COLS, PLANES>
406{
407 #[must_use]
416 pub const fn new() -> Self {
417 assert!(NROWS >= 1 && NROWS <= 32, "NROWS must be within 1..=32");
418 assert!(PLANES >= 1 && PLANES <= 8, "PLANES must be within 1..=8");
419 #[cfg(feature = "esp32-ordering")]
420 assert!(
421 COLS % 2 == 0,
422 "esp32-ordering feature requires COLS to be even"
423 );
424 let mut instance = Self {
425 rows: [RowData::new(); NROWS],
426 };
427 instance.format();
428 instance
429 }
430
431 #[must_use]
433 #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
434 pub const fn bcm_chunk_count() -> usize {
435 PLANES
436 }
437
438 #[must_use]
440 #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
441 pub const fn bcm_chunk_bytes() -> usize {
442 COLS * core::mem::size_of::<Entry>()
443 }
444
445 #[must_use]
447 pub const fn bcm_row_count() -> usize {
448 NROWS
449 }
450
451 #[must_use]
453 pub const fn bcm_row_bytes() -> usize {
454 GAP_BYTES + TRAILER_BYTES
455 }
456
457 #[inline]
466 pub const fn format(&mut self) {
467 let mut slot = 0;
468 while slot < NROWS {
469 let (prev_addr, addr) = slot_addresses::<NROWS>(slot);
470 self.rows[slot].format(prev_addr, addr);
471 slot += 1;
472 }
473 }
474
475 #[inline]
477 pub fn erase(&mut self) {
478 const MASK: u16 = !0b0111_1110_0000_0000; for row in &mut self.rows {
480 for p in 0..PLANES {
481 for entry in &mut row.plane_mut(p).data {
482 entry.mask_raw(MASK);
483 }
484 }
485 #[cfg(feature = "inter-row-blank")]
486 {
487 row.sync_plane0();
488 }
489 }
490 }
491
492 #[inline]
494 pub fn set_pixel(&mut self, p: Point, color: Color) {
495 if p.x < 0 || p.y < 0 {
496 return;
497 }
498 self.set_pixel_internal(p.x as usize, p.y as usize, color);
499 }
500
501 #[inline]
502 fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
503 if x >= COLS || y >= NROWS * 2 {
504 return;
505 }
506
507 #[cfg(feature = "skip-black-pixels")]
510 if color == Color::BLACK {
511 return;
512 }
513
514 let row_idx = map_row_index::<NROWS>(if y < NROWS { y } else { y - NROWS });
515 let is_top = y < NROWS;
516 let red = color.r();
517 let green = color.g();
518 let blue = color.b();
519
520 let col_idx = map_index(x);
521 for plane_idx in 0..PLANES {
522 let bit = (8 - PLANES + plane_idx) as u32;
523 let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
524 | ((u8::from(((green >> bit) & 1) != 0)) << 1)
525 | u8::from(((red >> bit) & 1) != 0);
526 let entry = &mut self.rows[row_idx].plane_mut(plane_idx).data[col_idx];
527 if is_top {
528 entry.set_color0_bits(bits);
529 } else {
530 entry.set_color1_bits(bits);
531 }
532 }
533 #[cfg(feature = "inter-row-blank")]
537 {
538 self.rows[row_idx].plane0.data[col_idx] = self.rows[row_idx].planes[0].data[col_idx];
539 }
540 }
541
542 #[must_use]
547 pub fn pixel_row_ptr_len(&self, row_idx: usize, plane_idx: usize) -> (*const u8, usize) {
548 assert!(
549 row_idx < NROWS,
550 "row_idx {row_idx} out of range for {NROWS} rows"
551 );
552 assert!(
553 plane_idx < PLANES,
554 "plane_idx {plane_idx} out of range for {PLANES} planes"
555 );
556 let ptr = (&raw const self.rows[row_idx].planes[plane_idx]).cast::<u8>();
557 let len = COLS * core::mem::size_of::<Entry>();
558 (ptr, len)
559 }
560
561 #[must_use]
571 pub fn gap_ptr_len(&self, row_idx: usize) -> (*const u8, usize) {
572 assert!(
573 row_idx < NROWS,
574 "row_idx {row_idx} out of range for {NROWS} rows"
575 );
576 #[cfg(feature = "inter-row-blank")]
577 let ptr = (&raw const self.rows[row_idx].gap).cast::<u8>();
578 #[cfg(not(feature = "inter-row-blank"))]
579 let ptr = (&raw const self.rows[row_idx].planes[0]).cast::<u8>();
580 (ptr, GAP_BYTES)
581 }
582
583 #[must_use]
591 pub fn trailer_ptr_len(&self, row_idx: usize) -> (*const u8, usize) {
592 assert!(
593 row_idx < NROWS,
594 "row_idx {row_idx} out of range for {NROWS} rows"
595 );
596 #[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
597 let ptr = (&raw const self.rows[row_idx].padding).cast::<u8>();
598 #[cfg(all(feature = "tail-closes-latch", not(feature = "esp32-ordering")))]
599 let ptr = (&raw const self.rows[row_idx].tail).cast::<u8>();
600 #[cfg(not(feature = "tail-closes-latch"))]
601 let ptr = (&raw const self.rows[row_idx].planes[0]).cast::<u8>();
602 (ptr, TRAILER_BYTES)
603 }
604}
605
606impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
607 for DmaFrameBuffer<NROWS, COLS, PLANES>
608{
609 fn default() -> Self {
610 Self::new()
611 }
612}
613
614impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
615 for DmaFrameBuffer<NROWS, COLS, PLANES>
616{
617 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
618 f.debug_struct("DmaFrameBuffer")
619 .field("size", &core::mem::size_of_val(self))
620 .field("row_count", &NROWS)
621 .field("planes", &PLANES)
622 .field("cols", &COLS)
623 .finish()
624 }
625}
626
627#[cfg(feature = "defmt")]
628impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
629 for DmaFrameBuffer<NROWS, COLS, PLANES>
630{
631 fn format(&self, f: defmt::Formatter) {
632 defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
633 defmt::write!(f, " size: {}", core::mem::size_of_val(self));
634 }
635}
636
637impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
638 for DmaFrameBuffer<NROWS, COLS, PLANES>
639{
640 type Word = u16;
641
642 const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY] = bcm_sequence::<COLS, PLANES>();
643
644 const BCM_SEQUENCE_LEN: usize = PLANES;
645
646 const BCM_SEQUENCE_COUNT: usize = NROWS;
647
648 const BCM_SEGMENTS_PER_GROUP: usize = PLANES;
649
650 fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
651 let segments_per_row = PLANES;
652 assert!(
653 index < NROWS * segments_per_row,
654 "segment index {index} out of range"
655 );
656 let row_idx = index / segments_per_row;
657 let plane_idx = index % segments_per_row;
658 #[cfg(feature = "inter-row-blank")]
659 {
660 if plane_idx == 0 {
661 return (&raw const self.rows[row_idx].plane0).cast::<u8>();
664 }
665 }
666 let (ptr, _) = self.pixel_row_ptr_len(row_idx, plane_idx);
667 ptr
668 }
669}
670
671impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
672 for DmaFrameBuffer<NROWS, COLS, PLANES>
673{
674 #[inline]
675 fn erase(&mut self) {
676 DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
677 }
678
679 #[inline]
680 fn set_pixel(&mut self, p: Point, color: Color) {
681 DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
682 }
683}
684
685impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
686 for DmaFrameBuffer<NROWS, COLS, PLANES>
687{
688}
689
690impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
691 for DmaFrameBuffer<NROWS, COLS, PLANES>
692{
693 fn size(&self) -> Size {
694 Size::new(COLS as u32, (NROWS * 2) as u32)
695 }
696}
697
698impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
699 for DmaFrameBuffer<NROWS, COLS, PLANES>
700{
701 type Color = Color;
702 type Error = Infallible;
703
704 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
705 where
706 I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
707 {
708 for pixel in pixels {
709 if pixel.0.x < 0 || pixel.0.y < 0 {
710 continue;
711 }
712 self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
713 }
714 Ok(())
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 extern crate std;
721
722 use super::super::{LEAD_BLANK_DELAY, OE_ACTIVE, TRAIL_BLANK_DELAY};
723 use super::*;
724 use embedded_graphics::prelude::*;
725 use std::format;
726
727 const TEST_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
728 128
729 } else {
730 64
731 };
732 const TEST_PLANES: usize = 8;
733 type TestBuffer = DmaFrameBuffer<16, TEST_COLS, TEST_PLANES>;
734
735 #[test]
736 fn format_sets_addresses_per_plane() {
737 let fb = TestBuffer::new();
738
739 for slot in 0..16 {
740 let (prev_addr, addr) = slot_addresses::<16>(slot);
741 let prev_addr = u16::from(prev_addr);
742
743 for col in 0..TEST_COLS {
745 let entry = fb.rows[slot].plane(0).data[col];
746 assert_eq!(
747 entry.addr(),
748 prev_addr,
749 "slot {slot} plane 0 col {col} should use prev_addr"
750 );
751 }
752
753 for plane_idx in 1..TEST_PLANES {
755 for col in 0..TEST_COLS {
756 let entry = fb.rows[slot].plane(plane_idx).data[col];
757 assert_eq!(
758 entry.addr(),
759 u16::from(addr),
760 "slot {slot} plane {plane_idx} col {col} should use current addr"
761 );
762 }
763 }
764 }
765 }
766
767 #[test]
768 fn planes_match_row_plane_template() {
769 let fb = TestBuffer::new();
770
771 for slot in 0..16 {
772 let (prev_addr, addr) = slot_addresses::<16>(slot);
773
774 let expected = make_row_plane_template::<TEST_COLS>(prev_addr, false, true);
777 assert_eq!(
778 fb.rows[slot].plane(0).data,
779 expected,
780 "slot {slot} plane 0 doesn't match template"
781 );
782
783 for p in 1..TEST_PLANES {
785 let expected = make_row_plane_template::<TEST_COLS>(addr, p == 1, false);
786 assert_eq!(
787 fb.rows[slot].plane(p).data,
788 expected,
789 "slot {slot} plane {p} doesn't match template"
790 );
791 }
792 }
793 }
794
795 #[test]
796 fn pixel_rows_have_latch_at_last_entry() {
797 let fb = TestBuffer::new();
798
799 for row in &fb.rows {
800 for p in 0..TEST_PLANES {
801 let plane = row.plane(p);
802 let last_idx = map_index(TEST_COLS - 1);
803 assert!(
804 plane.data[last_idx].latch(),
805 "last pixel must have latch set"
806 );
807 }
808 }
809 }
810
811 #[test]
812 fn first_plane_has_lead_blank_only() {
813 let fb = TestBuffer::new();
814 let oe_active = cfg!(feature = "invert-oe");
815
816 for row in &fb.rows {
817 let plane = row.plane(0);
818
819 let first_idx = map_index(0);
822 assert_eq!(
823 plane.data[first_idx].output_enable(),
824 oe_active,
825 "plane 0: first pixel should have OE active (no trail blank)"
826 );
827
828 let active_idx = map_index(TRAIL_BLANK_DELAY);
829 assert_eq!(
830 plane.data[active_idx].output_enable(),
831 oe_active,
832 "plane 0: pixel after trail blank position should have OE active"
833 );
834
835 let lead_idx = map_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
836 assert_eq!(
837 plane.data[lead_idx].output_enable(),
838 !oe_active,
839 "plane 0: lead blank should have OE blank"
840 );
841
842 let latch_idx = map_index(TEST_COLS - 1);
843 assert_eq!(
844 plane.data[latch_idx].output_enable(),
845 !oe_active,
846 "plane 0: latch should have OE blank"
847 );
848 }
849 }
850
851 #[test]
852 fn last_plane_blanks_only_at_latch() {
853 let fb = TestBuffer::new();
854 let oe_active = cfg!(feature = "invert-oe");
855 let last = TEST_PLANES - 1;
856
857 for row in &fb.rows {
858 let plane = row.plane(last);
859
860 let first_idx = map_index(0);
861 assert_eq!(
862 plane.data[first_idx].output_enable(),
863 oe_active,
864 "last plane: first pixel should have OE active (no trail blank)"
865 );
866
867 if LEAD_BLANK_DELAY > 0 {
870 let lead_idx = map_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
871 assert_eq!(
872 plane.data[lead_idx].output_enable(),
873 oe_active,
874 "last plane: near-end pixel should have OE active (no lead blank)"
875 );
876 }
877
878 let latch_idx = map_index(TEST_COLS - 1);
879 assert_eq!(
880 plane.data[latch_idx].output_enable(),
881 !oe_active,
882 "last plane: latch should have OE blank"
883 );
884 }
885 }
886
887 #[test]
888 fn middle_planes_have_oe_active_except_latch() {
889 let fb = TestBuffer::new();
890 let oe_active = cfg!(feature = "invert-oe");
891
892 for row in &fb.rows {
894 for plane_idx in 2..TEST_PLANES - 1 {
895 for col in 0..TEST_COLS {
896 let entry = row.plane(plane_idx).data[col];
897 if col == map_index(TEST_COLS - 1) {
898 assert_eq!(
899 entry.output_enable(),
900 !oe_active,
901 "middle plane {plane_idx}: latch should have OE blank"
902 );
903 } else {
904 assert_eq!(
905 entry.output_enable(),
906 oe_active,
907 "middle plane {plane_idx} col {col} should have OE active"
908 );
909 }
910 }
911 }
912 }
913 }
914
915 #[test]
916 fn plane_1_has_trail_blank_only() {
917 let fb = TestBuffer::new();
918 let oe_active = cfg!(feature = "invert-oe");
919
920 for row in &fb.rows {
921 let plane = row.plane(1);
922
923 if TRAIL_BLANK_DELAY > 0 {
924 let trail_idx = map_index(0);
925 assert_eq!(
926 plane.data[trail_idx].output_enable(),
927 !oe_active,
928 "plane 1: trail blank should have OE blank"
929 );
930 }
931
932 let active_idx = map_index(TRAIL_BLANK_DELAY);
933 assert_eq!(
934 plane.data[active_idx].output_enable(),
935 oe_active,
936 "plane 1: pixel after trail blank should have OE active"
937 );
938
939 let near_end = map_index(TEST_COLS - 2);
940 assert_eq!(
941 plane.data[near_end].output_enable(),
942 oe_active,
943 "plane 1: near-end pixel should have OE active (no lead blank)"
944 );
945
946 let latch_idx = map_index(TEST_COLS - 1);
947 assert_eq!(
948 plane.data[latch_idx].output_enable(),
949 !oe_active,
950 "plane 1: latch should have OE blank"
951 );
952 }
953 }
954
955 #[test]
956 fn single_plane_buffer_has_both_blanks_with_prev_addr() {
957 type OnePlane = DmaFrameBuffer<16, TEST_COLS, 1>;
958 let fb = OnePlane::new();
959 let oe_active = cfg!(feature = "invert-oe");
960
961 for slot in 0..16 {
962 let (prev_addr, _) = slot_addresses::<16>(slot);
963 let prev_addr = u16::from(prev_addr);
964 let plane = fb.rows[slot].plane(0);
965
966 assert_eq!(
968 plane.data[map_index(0)].addr(),
969 prev_addr,
970 "single plane slot {slot}: should use prev_addr"
971 );
972
973 if TRAIL_BLANK_DELAY > 0 {
974 let trail_idx = map_index(0);
975 assert_eq!(
976 plane.data[trail_idx].output_enable(),
977 !oe_active,
978 "single plane: trail blank should have OE blank"
979 );
980 }
981
982 if LEAD_BLANK_DELAY > 0 {
983 let lead_idx = map_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
984 assert_eq!(
985 plane.data[lead_idx].output_enable(),
986 !oe_active,
987 "single plane: lead blank should have OE blank"
988 );
989 }
990
991 let active_idx = map_index(TRAIL_BLANK_DELAY);
992 assert_eq!(
993 plane.data[active_idx].output_enable(),
994 oe_active,
995 "single plane: active pixel should have OE active"
996 );
997 }
998 }
999
1000 #[test]
1001 fn set_pixel_maps_top_half_bits_per_plane() {
1002 let mut fb = TestBuffer::new();
1003 let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
1004 fb.set_pixel(Point::new(2, 3), color);
1005
1006 for plane_idx in 0..TEST_PLANES {
1007 let bit = 8 - TEST_PLANES + plane_idx;
1008 let entry = fb.rows[map_row_index::<16>(3)].plane(plane_idx).data[map_index(2)];
1009 assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
1010 assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
1011 assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
1012 }
1013 }
1014
1015 #[test]
1016 fn set_pixel_maps_bottom_half_bits_per_plane() {
1017 let mut fb = TestBuffer::new();
1018 let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
1019 fb.set_pixel(Point::new(4, 20), color);
1020
1021 for plane_idx in 0..TEST_PLANES {
1022 let bit = 8 - TEST_PLANES + plane_idx;
1023 let entry = fb.rows[map_row_index::<16>(4)].plane(plane_idx).data[map_index(4)];
1024 assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
1025 assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
1026 assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
1027 }
1028 }
1029
1030 #[test]
1031 #[cfg(feature = "reverse-row-order")]
1032 fn reverse_row_order_stores_rows_back_to_front() {
1033 let mut fb = TestBuffer::new();
1034
1035 assert_eq!(fb.rows[0].plane(0).data[map_index(0)].addr(), 0);
1039 assert_eq!(fb.rows[0].plane(1).data[map_index(0)].addr(), 15);
1040 assert_eq!(fb.rows[15].plane(0).data[map_index(0)].addr(), 1);
1042 assert_eq!(fb.rows[15].plane(1).data[map_index(0)].addr(), 0);
1043
1044 fb.set_pixel(Point::new(2, 0), Color::RED);
1046 let col2 = map_index(2);
1047 assert!(fb.rows[15].plane(TEST_PLANES - 1).data[col2].red1());
1048 assert!(!fb.rows[0].plane(TEST_PLANES - 1).data[col2].red1());
1049 }
1050
1051 #[test]
1052 fn erase_clears_only_color_bits() {
1053 let mut fb = TestBuffer::new();
1054 let oe_before = fb.rows[map_row_index::<16>(0)].plane(0).data[map_index(1)].output_enable();
1055 fb.set_pixel(Point::new(0, 0), Color::WHITE);
1056 fb.erase();
1057
1058 for row in &fb.rows {
1059 for p in 0..TEST_PLANES {
1060 let plane = row.plane(p);
1061 for entry in &plane.data {
1062 assert!(!entry.red1());
1063 assert!(!entry.grn1());
1064 assert!(!entry.blu1());
1065 assert!(!entry.red2());
1066 assert!(!entry.grn2());
1067 assert!(!entry.blu2());
1068 }
1069 }
1070 }
1071
1072 assert_eq!(
1073 fb.rows[map_row_index::<16>(0)].plane(0).data[map_index(1)].output_enable(),
1074 oe_before
1075 );
1076 }
1077
1078 #[test]
1079 fn erase_preserves_latch_and_oe_pattern() {
1080 let fb_fresh = TestBuffer::new();
1081 let mut fb = TestBuffer::new();
1082 fb.set_pixel(Point::new(0, 0), Color::WHITE);
1083 fb.erase();
1084
1085 for row_idx in 0..16 {
1086 for plane_idx in 0..TEST_PLANES {
1087 for col in 0..TEST_COLS {
1088 let entry = fb.rows[row_idx].plane(plane_idx).data[col];
1089 let fresh = fb_fresh.rows[row_idx].plane(plane_idx).data[col];
1090 assert_eq!(
1091 entry.output_enable(),
1092 fresh.output_enable(),
1093 "OE mismatch at row {row_idx} plane {plane_idx} col {col}"
1094 );
1095 assert_eq!(
1096 entry.latch(),
1097 fresh.latch(),
1098 "latch mismatch at row {row_idx} plane {plane_idx} col {col}"
1099 );
1100 assert_eq!(
1101 entry.addr(),
1102 fresh.addr(),
1103 "addr mismatch at row {row_idx} plane {plane_idx} col {col}"
1104 );
1105 }
1106 }
1107 }
1108 }
1109
1110 #[cfg(feature = "inter-row-blank")]
1111 #[test]
1112 fn erase_preserves_gap() {
1113 let fb_fresh = TestBuffer::new();
1114 let mut fb = TestBuffer::new();
1115 fb.set_pixel(Point::new(0, 0), Color::WHITE);
1116 fb.erase();
1117
1118 for row_idx in 0..16 {
1119 assert_eq!(
1120 fb.rows[row_idx].gap, fb_fresh.rows[row_idx].gap,
1121 "gap mismatch at row {row_idx}"
1122 );
1123 }
1124 }
1125
1126 #[cfg(feature = "inter-row-blank")]
1127 #[test]
1128 fn gap_entries_hold_prev_addr_and_oe_blank() {
1129 let fb = TestBuffer::new();
1130 for (slot, row) in fb.rows.iter().enumerate() {
1131 let (prev_addr, _) = slot_addresses::<16>(slot);
1132 let prev_addr = u16::from(prev_addr);
1133 for entry in &row.gap {
1134 assert_eq!(
1135 entry.addr(),
1136 prev_addr,
1137 "gap entry should hold prev_addr at slot {slot}"
1138 );
1139 assert_eq!(
1140 entry.0 & 0b1_0000_0000,
1141 OE_BLANK,
1142 "gap entry should have OE blank at slot {slot}"
1143 );
1144 assert!(!entry.latch(), "gap entry must not latch at slot {slot}");
1145 }
1146 }
1147 }
1148
1149 #[test]
1150 #[allow(deprecated)]
1151 fn bcm_const_fns_return_expected_values() {
1152 assert_eq!(TestBuffer::bcm_chunk_count(), TEST_PLANES);
1153 assert_eq!(
1154 TestBuffer::bcm_chunk_bytes(),
1155 TEST_COLS * core::mem::size_of::<Entry>()
1156 );
1157 assert_eq!(TestBuffer::bcm_row_count(), 16);
1158 assert_eq!(TestBuffer::bcm_row_bytes(), GAP_BYTES + TRAILER_BYTES);
1159 }
1160
1161 #[test]
1162 fn draw_target_iter_sets_pixels() {
1163 let mut fb = TestBuffer::new();
1164 let pixels = [Pixel(Point::new(1, 1), Color::RED)];
1165 let result = fb.draw_iter(pixels);
1166 assert!(result.is_ok());
1167
1168 for plane_idx in 0..TEST_PLANES {
1169 let bit = 8 - TEST_PLANES + plane_idx;
1170 let entry = fb.rows[map_row_index::<16>(1)].plane(plane_idx).data[map_index(1)];
1171 assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
1172 assert!(!entry.grn1());
1173 assert!(!entry.blu1());
1174 }
1175 }
1176
1177 #[test]
1178 fn set_pixel_ignores_out_of_bounds_and_negative() {
1179 let mut fb = TestBuffer::new();
1180 let before = fb.rows;
1181 fb.set_pixel(Point::new(-1, 0), Color::WHITE);
1182 fb.set_pixel(Point::new(0, -1), Color::WHITE);
1183 fb.set_pixel(Point::new(TEST_COLS as i32, 0), Color::WHITE);
1184 fb.set_pixel(Point::new(0, 32), Color::WHITE);
1185 assert_eq!(fb.rows, before);
1186 }
1187
1188 #[test]
1189 #[cfg(feature = "skip-black-pixels")]
1190 fn test_skip_black_pixels_enabled() {
1191 let mut fb = TestBuffer::new();
1192
1193 fb.set_pixel_internal(10, 5, Color::RED);
1195
1196 let mapped_col_10 = map_index(10);
1198 assert!(fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].red1());
1199 assert!(!fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].grn1());
1200 assert!(!fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].blu1());
1201
1202 fb.set_pixel_internal(10, 5, Color::BLACK);
1204
1205 assert!(fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].red1());
1207 assert!(!fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].grn1());
1208 assert!(!fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].blu1());
1209 }
1210
1211 #[test]
1212 #[cfg(not(feature = "skip-black-pixels"))]
1213 fn test_skip_black_pixels_disabled() {
1214 let mut fb = TestBuffer::new();
1215
1216 fb.set_pixel_internal(10, 5, Color::RED);
1218
1219 let mapped_col_10 = map_index(10);
1221 assert!(fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].red1());
1222 assert!(!fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].grn1());
1223 assert!(!fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].blu1());
1224
1225 fb.set_pixel_internal(10, 5, Color::BLACK);
1227
1228 assert!(!fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].red1());
1230 assert!(!fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].grn1());
1231 assert!(!fb.rows[map_row_index::<16>(5)].plane(0).data[mapped_col_10].blu1());
1232 }
1233
1234 #[test]
1235 #[should_panic(expected = "NROWS must be within 1..=32")]
1236 fn new_panics_for_too_many_rows() {
1237 let _ = DmaFrameBuffer::<33, TEST_COLS, 8>::new();
1238 }
1239
1240 #[test]
1241 #[should_panic(expected = "PLANES must be within 1..=8")]
1242 fn new_panics_for_too_many_planes() {
1243 let _ = DmaFrameBuffer::<16, TEST_COLS, 9>::new();
1244 }
1245
1246 #[test]
1247 fn pixel_row_ptr_len_returns_correct_pointers() {
1248 let fb = TestBuffer::new();
1249 let (ptr, len) = fb.pixel_row_ptr_len(0, 0);
1250 assert_eq!(len, TEST_COLS * core::mem::size_of::<Entry>());
1251 assert_eq!(ptr, (&raw const fb.rows[0].planes[0]).cast::<u8>());
1252 }
1253
1254 #[test]
1255 fn gap_ptr_len_returns_correct_pointers() {
1256 let fb = TestBuffer::new();
1257 let (ptr, len) = fb.gap_ptr_len(0);
1258 assert_eq!(len, GAP_BYTES);
1259 #[cfg(feature = "inter-row-blank")]
1260 assert_eq!(ptr, (&raw const fb.rows[0].gap).cast::<u8>());
1261 #[cfg(not(feature = "inter-row-blank"))]
1262 assert_eq!(ptr, (&raw const fb.rows[0].planes[0]).cast::<u8>());
1263 }
1264
1265 #[test]
1266 fn trailer_ptr_len_returns_correct_pointers() {
1267 let fb = TestBuffer::new();
1268 let (ptr, len) = fb.trailer_ptr_len(0);
1269 assert_eq!(len, TRAILER_BYTES);
1270 #[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
1271 assert_eq!(ptr, (&raw const fb.rows[0].padding).cast::<u8>());
1272 #[cfg(all(feature = "tail-closes-latch", not(feature = "esp32-ordering")))]
1273 assert_eq!(ptr, (&raw const fb.rows[0].tail).cast::<u8>());
1274 #[cfg(not(feature = "tail-closes-latch"))]
1275 assert_eq!(ptr, (&raw const fb.rows[0].planes[0]).cast::<u8>());
1276 }
1277
1278 #[test]
1279 #[should_panic(expected = "out of range")]
1280 fn pixel_row_ptr_len_panics_for_invalid_row() {
1281 let fb = TestBuffer::new();
1282 let _ = fb.pixel_row_ptr_len(16, 0);
1283 }
1284
1285 #[test]
1286 #[should_panic(expected = "out of range")]
1287 fn pixel_row_ptr_len_panics_for_invalid_plane() {
1288 let fb = TestBuffer::new();
1289 let _ = fb.pixel_row_ptr_len(0, 8);
1290 }
1291
1292 #[test]
1293 #[should_panic(expected = "out of range")]
1294 fn trailer_ptr_len_panics_for_invalid_row() {
1295 let fb = TestBuffer::new();
1296 let _ = fb.trailer_ptr_len(16);
1297 }
1298
1299 #[test]
1300 fn origin_dimensions_match_panel_geometry() {
1301 let fb = TestBuffer::new();
1302 assert_eq!(fb.size(), Size::new(TEST_COLS as u32, 32));
1303 }
1304
1305 #[test]
1306 fn default_matches_new() {
1307 let fb_default = TestBuffer::default();
1308 let fb_new = TestBuffer::new();
1309 assert_eq!(fb_default.rows, fb_new.rows);
1310 }
1311
1312 #[test]
1313 fn debug_impl_includes_shape_information() {
1314 let fb = TestBuffer::new();
1315 let s = format!("{fb:?}");
1316 assert!(s.contains("DmaFrameBuffer"));
1317 assert!(s.contains("row_count"));
1318 assert!(s.contains("planes"));
1319 }
1320
1321 static STATIC_FB: TestBuffer = TestBuffer::new();
1322
1323 #[test]
1324 fn static_construction_is_formatted() {
1325 let runtime_fb = TestBuffer::new();
1326
1327 for (ri, row) in STATIC_FB.rows.iter().enumerate() {
1328 #[cfg(feature = "inter-row-blank")]
1329 {
1330 assert_eq!(
1331 row.plane0, runtime_fb.rows[ri].plane0,
1332 "static vs runtime plane0 mismatch at row {ri}"
1333 );
1334 assert_eq!(
1335 row.gap, runtime_fb.rows[ri].gap,
1336 "static vs runtime gap mismatch at row {ri}"
1337 );
1338 }
1339 assert_eq!(
1340 row.planes, runtime_fb.rows[ri].planes,
1341 "static vs runtime planes mismatch at row {ri}"
1342 );
1343 }
1344 }
1345
1346 #[test]
1347 fn format_reinitializes_at_runtime() {
1348 let mut fb = TestBuffer::new();
1349 fb.erase();
1350 fb.format();
1351
1352 for (ri, row) in fb.rows.iter().enumerate() {
1353 #[cfg(feature = "inter-row-blank")]
1354 {
1355 assert_eq!(
1356 row.plane0, STATIC_FB.rows[ri].plane0,
1357 "re-formatted vs static plane0 mismatch at row {ri}"
1358 );
1359 assert_eq!(
1360 row.gap, STATIC_FB.rows[ri].gap,
1361 "re-formatted vs static gap mismatch at row {ri}"
1362 );
1363 }
1364 assert_eq!(
1365 row.planes, STATIC_FB.rows[ri].planes,
1366 "re-formatted vs static planes mismatch at row {ri}"
1367 );
1368 }
1369 }
1370
1371 #[test]
1372 fn framebuffer_operations_trait_delegates_correctly() {
1373 let mut fb = TestBuffer::new();
1374 FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
1375
1376 assert!(fb.rows[map_row_index::<16>(5)].plane(TEST_PLANES - 1).data[map_index(3)].grn1());
1378
1379 FrameBufferOperations::erase(&mut fb);
1380 for row in &fb.rows {
1381 for p in 0..TEST_PLANES {
1382 let plane = row.plane(p);
1383 for entry in &plane.data {
1384 assert!(!entry.red1());
1385 assert!(!entry.grn1());
1386 assert!(!entry.blu1());
1387 assert!(!entry.red2());
1388 assert!(!entry.grn2());
1389 assert!(!entry.blu2());
1390 }
1391 }
1392 }
1393 }
1394
1395 #[test]
1396 fn row_data_size_matches_expected_layout() {
1397 let expected_pixels = core::mem::size_of::<PixelPlane<TEST_COLS>>()
1398 * (TEST_PLANES + usize::from(cfg!(feature = "inter-row-blank")));
1399 let expected_overhead = GAP_BYTES + TRAILER_BYTES;
1400 assert_eq!(
1401 core::mem::size_of::<RowData<TEST_COLS, 8>>(),
1402 expected_pixels + expected_overhead
1403 );
1404 }
1405
1406 #[test]
1407 fn bcm_segment_count_equals_rows_times_planes() {
1408 use crate::FrameBuffer;
1409 let fb = TestBuffer::new();
1410 assert_eq!(fb.bcm_segment_count(), 16 * TEST_PLANES);
1411 }
1412
1413 #[test]
1414 fn bcm_segments_per_group_equals_planes() {
1415 use crate::FrameBuffer;
1416 let fb = TestBuffer::new();
1417 assert_eq!(fb.bcm_segments_per_group(), TEST_PLANES);
1418 assert_eq!(
1419 fb.bcm_segment_count() % fb.bcm_segments_per_group(),
1420 0,
1421 "segment count must be divisible by segments_per_group"
1422 );
1423 }
1424
1425 #[test]
1426 fn bcm_segments_interleave_pixel_and_gap() {
1427 use crate::FrameBuffer;
1428 let fb = TestBuffer::new();
1429 let segments_per_row = TEST_PLANES;
1430
1431 for row in 0..16usize {
1432 let seg = fb.bcm_segment(row * segments_per_row);
1436 let plane_len = TEST_COLS * core::mem::size_of::<Entry>();
1437 let len0 = if cfg!(feature = "inter-row-blank") {
1438 plane_len + GAP_BYTES
1439 } else {
1440 plane_len * TEST_PLANES + TRAILER_BYTES
1441 };
1442 #[cfg(feature = "inter-row-blank")]
1443 {
1444 assert_eq!(
1447 seg.ptr,
1448 (&raw const fb.rows[row].plane0).cast::<u8>(),
1449 "wrong ptr at row {row} plane 0"
1450 );
1451 }
1452 #[cfg(not(feature = "inter-row-blank"))]
1453 {
1454 let (ptr, _) = fb.pixel_row_ptr_len(row, 0);
1455 assert_eq!(seg.ptr, ptr, "wrong ptr at row {row} plane 0");
1456 }
1457 assert_eq!(seg.len, len0, "wrong len at row {row} plane 0");
1458 assert_eq!(seg.reps, 1, "wrong reps at row {row} plane 0");
1459
1460 for plane in 1..TEST_PLANES {
1462 let seg = fb.bcm_segment(row * segments_per_row + plane);
1463 let (ptr, plane_len) = fb.pixel_row_ptr_len(row, plane);
1464 let expected_reps = if cfg!(feature = "inter-row-blank") && plane == 1 {
1465 2
1466 } else {
1467 1 << (plane - 1)
1468 };
1469 assert_eq!(seg.ptr, ptr, "wrong ptr at row {row} plane {plane}");
1470 assert_eq!(
1471 seg.len,
1472 plane_len * (TEST_PLANES - plane) + TRAILER_BYTES,
1473 "wrong len at row {row} plane {plane}"
1474 );
1475 assert_eq!(
1476 seg.reps, expected_reps,
1477 "wrong reps at row {row} plane {plane}"
1478 );
1479 }
1480 }
1481 }
1482
1483 #[test]
1484 fn bcm_segment_total_reps_per_row() {
1485 use crate::FrameBuffer;
1486 let fb = TestBuffer::new();
1487 let segments_per_row = TEST_PLANES;
1488
1489 for row in 0..16usize {
1490 let mut pixel_reps = 0;
1494 for plane in 0..TEST_PLANES {
1495 pixel_reps += fb.bcm_segment(row * segments_per_row + plane).reps;
1496 }
1497 let expected = if cfg!(feature = "inter-row-blank") {
1498 (1 << (TEST_PLANES - 1)) + 1
1499 } else {
1500 1 << (TEST_PLANES - 1)
1501 };
1502 assert_eq!(pixel_reps, expected, "total pixel reps wrong for row {row}");
1503 }
1504 }
1505
1506 #[test]
1507 fn bcm_segment_plane_coverage_matches_bcm_weights() {
1508 use crate::FrameBuffer;
1509 let fb = TestBuffer::new();
1510 let segments_per_row = TEST_PLANES;
1511
1512 for row in 0..16usize {
1513 for plane in 0..TEST_PLANES {
1514 let mut coverage = 0;
1518 for first in 0..=plane {
1519 let seg = fb.bcm_segment(row * segments_per_row + first);
1520 let (covered, _) = plane_suffix(first, TEST_PLANES);
1521 if first + covered > plane {
1522 coverage += seg.reps;
1523 }
1524 }
1525 assert_eq!(
1526 coverage,
1527 1 << plane,
1528 "plane {plane} coverage wrong for row {row}"
1529 );
1530 }
1531 }
1532 }
1533
1534 #[test]
1535 #[should_panic(expected = "out of range")]
1536 fn bcm_segment_panics_for_invalid_index() {
1537 use crate::FrameBuffer;
1538 let fb = TestBuffer::new();
1539 let _ = fb.bcm_segment(16 * TEST_PLANES);
1540 }
1541
1542 #[test]
1543 fn bcm_sequence_matches_runtime_segments() {
1544 use crate::FrameBuffer;
1545 let fb = TestBuffer::new();
1546 assert_eq!(
1547 TestBuffer::BCM_SEGMENT_COUNT,
1548 TestBuffer::BCM_SEQUENCE_LEN * TestBuffer::BCM_SEQUENCE_COUNT
1549 );
1550 assert_eq!(fb.bcm_segment_count(), TestBuffer::BCM_SEGMENT_COUNT);
1551 assert_eq!(
1552 fb.bcm_segments_per_group(),
1553 TestBuffer::BCM_SEGMENTS_PER_GROUP
1554 );
1555 assert_eq!(
1556 TestBuffer::BCM_SEQUENCE_LEN % TestBuffer::BCM_SEGMENTS_PER_GROUP,
1557 0
1558 );
1559 for i in 0..TestBuffer::BCM_SEGMENT_COUNT {
1560 let entry = TestBuffer::BCM_SEQUENCE[i % TestBuffer::BCM_SEQUENCE_LEN];
1561 let seg = fb.bcm_segment(i);
1562 assert_eq!((seg.len, seg.reps), (entry.len, entry.reps), "segment {i}");
1563 assert!(!seg.ptr.is_null(), "segment {i} has null pointer");
1564 }
1565 for entry in &TestBuffer::BCM_SEQUENCE[TestBuffer::BCM_SEQUENCE_LEN..] {
1566 assert_eq!(*entry, crate::BcmLenReps::ZERO, "padding must be zero");
1567 }
1568 }
1569
1570 #[test]
1571 fn bcm_segment_data_contains_written_pixels() {
1572 use crate::FrameBuffer;
1573
1574 const TRINITY_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
1575 128
1576 } else {
1577 64
1578 };
1579 type TrinityBuf = DmaFrameBuffer<32, TRINITY_COLS, 5>;
1580 let mut fb = TrinityBuf::new();
1581
1582 fb.set_pixel(Point::new(10, 0), Color::WHITE);
1583
1584 let col_idx = map_index(10);
1585 let row0_base = map_row_index::<32>(0) * 5;
1587
1588 for plane_idx in 0..5usize {
1589 let seg = fb.bcm_segment(row0_base + plane_idx);
1591
1592 let data = unsafe { core::slice::from_raw_parts(seg.ptr as *const u16, TRINITY_COLS) };
1593 let raw = data[col_idx];
1594 let entry = Entry::from_raw(raw);
1595
1596 let bit = 8 - 5 + plane_idx; let expect_r = (0xFF >> bit) & 1 != 0;
1598 let expect_g = (0xFF >> bit) & 1 != 0;
1599 let expect_b = (0xFF >> bit) & 1 != 0;
1600 assert_eq!(
1601 entry.red1(),
1602 expect_r,
1603 "plane {plane_idx} (bit {bit}) R1 mismatch: raw={raw:#06x}"
1604 );
1605 assert_eq!(
1606 entry.grn1(),
1607 expect_g,
1608 "plane {plane_idx} (bit {bit}) G1 mismatch: raw={raw:#06x}"
1609 );
1610 assert_eq!(
1611 entry.blu1(),
1612 expect_b,
1613 "plane {plane_idx} (bit {bit}) B1 mismatch: raw={raw:#06x}"
1614 );
1615 }
1616 }
1617
1618 #[test]
1619 fn bcm_segment_data_contains_gradient_pixels() {
1620 use crate::FrameBuffer;
1621
1622 const TRINITY_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
1623 128
1624 } else {
1625 64
1626 };
1627 type TrinityBuf = DmaFrameBuffer<32, TRINITY_COLS, 5>;
1628 let mut fb = TrinityBuf::new();
1629
1630 let step: u8 = (256 / TRINITY_COLS) as u8;
1631 for x in 0..TRINITY_COLS {
1632 let brightness = (x as u8).wrapping_mul(step);
1633 fb.set_pixel(Point::new(x as i32, 0), Color::new(brightness, 0, 0));
1634 }
1635
1636 let mut any_nonzero = false;
1637 let row0_base = map_row_index::<32>(0) * 5;
1639 for plane_idx in 0..5usize {
1640 let seg = fb.bcm_segment(row0_base + plane_idx);
1642 let data = unsafe { core::slice::from_raw_parts(seg.ptr as *const u16, TRINITY_COLS) };
1643
1644 for x in 0..TRINITY_COLS {
1645 let col_idx = map_index(x);
1646 let entry = Entry::from_raw(data[col_idx]);
1647 let brightness = (x as u8).wrapping_mul(step);
1648 let bit = 8 - 5 + plane_idx; let expect_r = (brightness >> bit) & 1 != 0;
1650 assert_eq!(
1651 entry.red1(),
1652 expect_r,
1653 "x={x} plane={plane_idx} bit={bit} brightness={brightness} \
1654 raw={:#06x}",
1655 data[col_idx]
1656 );
1657 if expect_r {
1658 any_nonzero = true;
1659 }
1660 }
1661 }
1662 assert!(
1663 any_nonzero,
1664 "gradient should produce at least some non-zero color bits"
1665 );
1666 }
1667
1668 #[test]
1669 fn make_row_plane_template_both_false_blanks_only_latch() {
1670 let t = make_row_plane_template::<TEST_COLS>(5, false, false);
1671 for i in 0..TEST_COLS {
1672 let entry = t[map_index(i)];
1673 assert_eq!(entry.addr(), 5);
1674 if i == TEST_COLS - 1 {
1675 assert!(entry.latch(), "last entry must latch");
1676 assert_eq!(
1677 entry.0 & 0b1_0000_0000,
1678 OE_BLANK,
1679 "latch should be OE blank"
1680 );
1681 } else {
1682 assert!(!entry.latch(), "col {i} must not latch");
1683 assert_eq!(
1684 entry.0 & 0b1_0000_0000,
1685 OE_ACTIVE,
1686 "col {i} should be OE active"
1687 );
1688 }
1689 }
1690 }
1691
1692 #[test]
1693 fn make_row_plane_template_trail_only() {
1694 let t = make_row_plane_template::<TEST_COLS>(5, true, false);
1695 for i in 0..TEST_COLS {
1696 let entry = t[map_index(i)];
1697 if i < TRAIL_BLANK_DELAY || i == TEST_COLS - 1 {
1698 assert_eq!(
1699 entry.0 & 0b1_0000_0000,
1700 OE_BLANK,
1701 "col {i} should be OE blank"
1702 );
1703 } else {
1704 assert_eq!(
1705 entry.0 & 0b1_0000_0000,
1706 OE_ACTIVE,
1707 "col {i} should be OE active"
1708 );
1709 }
1710 }
1711 }
1712
1713 #[test]
1714 fn make_row_plane_template_lead_only() {
1715 let t = make_row_plane_template::<TEST_COLS>(5, false, true);
1716 for i in 0..TEST_COLS {
1717 let entry = t[map_index(i)];
1718 if i >= TEST_COLS.saturating_sub(LEAD_BLANK_DELAY + 1) {
1719 assert_eq!(
1720 entry.0 & 0b1_0000_0000,
1721 OE_BLANK,
1722 "col {i} should be lead blank"
1723 );
1724 } else {
1725 assert_eq!(
1726 entry.0 & 0b1_0000_0000,
1727 OE_ACTIVE,
1728 "col {i} should be OE active"
1729 );
1730 }
1731 }
1732 }
1733}