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