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]
329 pub const fn new() -> Self {
330 assert!(NROWS >= 1 && NROWS <= 32, "NROWS must be within 1..=32");
331 assert!(PLANES >= 1 && PLANES <= 8, "PLANES must be within 1..=8");
332 #[cfg(feature = "esp32-ordering")]
333 assert!(
334 COLS % 4 == 0,
335 "esp32-ordering feature requires COLS to be divisible by 4"
336 );
337 let mut instance = Self {
338 rows: [RowData::new(); NROWS],
339 };
340 instance.format();
341 instance
342 }
343
344 #[must_use]
346 #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
347 pub const fn bcm_chunk_count() -> usize {
348 PLANES
349 }
350
351 #[must_use]
354 #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
355 pub const fn bcm_chunk_bytes() -> usize {
356 core::mem::size_of::<PlaneRow<COLS>>()
357 }
358
359 #[must_use]
361 pub const fn bcm_row_count() -> usize {
362 NROWS
363 }
364
365 #[must_use]
367 pub const fn bcm_row_bytes() -> usize {
368 GAP_BYTES
369 }
370
371 #[inline]
377 pub const fn format(&mut self) {
378 let mut slot = 0;
379 while slot < NROWS {
380 let (_, addr) = slot_addresses::<NROWS>(slot);
381 self.rows[slot].format(addr);
382 slot += 1;
383 }
384 }
385
386 #[inline]
388 pub fn erase(&mut self) {
389 const MASK: u8 = !0b0011_1111;
390 for row in &mut self.rows {
391 for plane in &mut row.planes {
392 for entry in &mut plane.data {
393 entry.0 &= MASK;
394 }
395 }
396 }
397 }
398
399 #[inline]
401 pub fn set_pixel(&mut self, p: Point, color: Color) {
402 if p.x < 0 || p.y < 0 {
403 return;
404 }
405 self.set_pixel_internal(p.x as usize, p.y as usize, color);
406 }
407
408 #[inline]
409 fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
410 if x >= COLS || y >= NROWS * 2 {
411 return;
412 }
413
414 #[cfg(feature = "skip-black-pixels")]
417 if color == Color::BLACK {
418 return;
419 }
420
421 let row_idx = map_row_index::<NROWS>(if y < NROWS { y } else { y - NROWS });
422 let is_top = y < NROWS;
423 let red = color.r();
424 let green = color.g();
425 let blue = color.b();
426
427 for plane_idx in 0..PLANES {
428 let bit = (8 - PLANES + plane_idx) as u32;
429 let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
430 | ((u8::from(((green >> bit) & 1) != 0)) << 1)
431 | u8::from(((red >> bit) & 1) != 0);
432 let col_idx = map_index(x);
433 let entry = &mut self.rows[row_idx].planes[plane_idx].data[col_idx];
434 if is_top {
435 entry.set_color0_bits(bits);
436 } else {
437 entry.set_color1_bits(bits);
438 }
439 }
440 }
441
442 #[must_use]
448 pub fn pixel_row_ptr_len(&self, row_idx: usize, plane_idx: usize) -> (*const u8, usize) {
449 assert!(
450 row_idx < NROWS,
451 "row_idx {row_idx} out of range for {NROWS} rows"
452 );
453 assert!(
454 plane_idx < PLANES,
455 "plane_idx {plane_idx} out of range for {PLANES} planes"
456 );
457 let ptr = (&raw const self.rows[row_idx].planes[plane_idx]).cast::<u8>();
458 let len = core::mem::size_of::<PlaneRow<COLS>>();
459 (ptr, len)
460 }
461
462 #[must_use]
471 pub fn gap_ptr_len(&self, row_idx: usize) -> (*const u8, usize) {
472 assert!(
473 row_idx < NROWS,
474 "row_idx {row_idx} out of range for {NROWS} rows"
475 );
476 let ptr = (&raw const self.rows[row_idx].gap).cast::<u8>();
477 (ptr, GAP_BYTES)
478 }
479}
480
481impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
482 for DmaFrameBuffer<NROWS, COLS, PLANES>
483{
484 fn default() -> Self {
485 Self::new()
486 }
487}
488
489impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
490 for DmaFrameBuffer<NROWS, COLS, PLANES>
491{
492 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
493 f.debug_struct("DmaFrameBuffer")
494 .field("size", &core::mem::size_of_val(self))
495 .field("row_count", &NROWS)
496 .field("planes", &PLANES)
497 .field("cols", &COLS)
498 .finish()
499 }
500}
501
502#[cfg(feature = "defmt")]
503impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
504 for DmaFrameBuffer<NROWS, COLS, PLANES>
505{
506 fn format(&self, f: defmt::Formatter) {
507 defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
508 defmt::write!(f, " size: {}", core::mem::size_of_val(self));
509 }
510}
511
512impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
513 for DmaFrameBuffer<NROWS, COLS, PLANES>
514{
515 type Word = u8;
516
517 const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY] = bcm_sequence::<COLS, PLANES>();
518
519 const BCM_SEQUENCE_LEN: usize = PLANES + HAS_GAP as usize;
520
521 const BCM_SEQUENCE_COUNT: usize = NROWS;
522
523 const BCM_SEGMENTS_PER_GROUP: usize = PLANES + HAS_GAP as usize;
524
525 fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
526 let gap = usize::from(HAS_GAP);
527 let segments_per_row = PLANES + gap;
528 assert!(
529 index < NROWS * segments_per_row,
530 "segment index {index} out of range"
531 );
532 let row_idx = index / segments_per_row;
533 let within_row = index % segments_per_row;
534 if within_row == 0 {
535 let (ptr, _) = self.pixel_row_ptr_len(row_idx, 0);
536 ptr
537 } else if HAS_GAP && within_row == 1 {
538 let (ptr, _) = self.gap_ptr_len(row_idx);
539 ptr
540 } else {
541 let plane_idx = within_row - gap;
542 let (ptr, _) = self.pixel_row_ptr_len(row_idx, plane_idx);
543 ptr
544 }
545 }
546}
547
548impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
549 for DmaFrameBuffer<NROWS, COLS, PLANES>
550{
551 #[inline]
552 fn erase(&mut self) {
553 DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
554 }
555
556 #[inline]
557 fn set_pixel(&mut self, p: Point, color: Color) {
558 DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
559 }
560}
561
562impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
563 for DmaFrameBuffer<NROWS, COLS, PLANES>
564{
565}
566
567impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
568 for DmaFrameBuffer<NROWS, COLS, PLANES>
569{
570 fn size(&self) -> Size {
571 Size::new(COLS as u32, (NROWS * 2) as u32)
572 }
573}
574
575impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
576 for DmaFrameBuffer<NROWS, COLS, PLANES>
577{
578 type Color = Color;
579 type Error = Infallible;
580
581 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
582 where
583 I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
584 {
585 for pixel in pixels {
586 if pixel.0.x < 0 || pixel.0.y < 0 {
587 continue;
588 }
589 self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
590 }
591 Ok(())
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 extern crate std;
598
599 use super::super::{LEAD_BLANK_DELAY, TRAIL_BLANK_DELAY};
600 use super::*;
601 use embedded_graphics::prelude::*;
602 use std::format;
603
604 const TEST_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
605 128
606 } else {
607 64
608 };
609 const TEST_PLANES: usize = 8;
610 type TestBuffer = DmaFrameBuffer<16, TEST_COLS, TEST_PLANES>;
611
612 #[test]
613 fn format_sets_current_row_address_for_all_planes() {
614 let fb = TestBuffer::new();
615
616 for slot in 0..16 {
617 let (_, addr) = slot_addresses::<16>(slot);
618 for plane_idx in 0..TEST_PLANES {
619 let plane = &fb.rows[slot].planes[plane_idx];
620 assert_eq!(
621 plane.address[map_index(0)].addr(),
622 addr,
623 "slot {slot} plane {plane_idx} addr byte 0 should be the current row"
624 );
625 assert_eq!(
626 plane.address[map_index(1)].addr(),
627 addr,
628 "slot {slot} plane {plane_idx} addr byte 1 should be the current row"
629 );
630 assert_eq!(
631 plane.address[map_index(2)].addr(),
632 addr,
633 "slot {slot} plane {plane_idx} addr byte 2 should be the current row"
634 );
635 assert_eq!(
636 plane.address[map_index(3)].0,
637 OE_BLANK,
638 "slot {slot} plane {plane_idx} addr byte 3 should be blanked with addr 0"
639 );
640 }
641 }
642 }
643
644 #[test]
645 fn address_bytes_have_two_latch_pulses() {
646 let fb = TestBuffer::new();
647
648 for row in &fb.rows {
649 for plane in &row.planes {
650 let latch_count = plane.address.iter().filter(|a| a.latch()).count();
651 assert_eq!(latch_count, 2, "expected exactly two latch pulses");
652 assert!(plane.address[map_index(0)].latch());
653 assert!(plane.address[map_index(1)].latch());
654 }
655 }
656 }
657
658 #[test]
659 fn planes_match_row_plane_template() {
660 let fb = TestBuffer::new();
661
662 for row_idx in 0..16 {
663 for p in 0..TEST_PLANES {
664 let expected = make_row_plane_template::<TEST_COLS>(p == 1, p == 0);
665 assert_eq!(
666 fb.rows[row_idx].planes[p].data, expected,
667 "row {row_idx} plane {p} doesn't match template"
668 );
669 }
670 }
671 }
672
673 #[test]
674 fn first_plane_has_lead_blank_only() {
675 let fb = TestBuffer::new();
676 let oe_active = !cfg!(feature = "invert-oe");
677
678 for row in &fb.rows {
679 let plane = &row.planes[0];
680
681 let first_idx = map_index(0);
685 assert_eq!(
686 plane.data[first_idx].output_enable(),
687 oe_active,
688 "plane 0: first pixel should have OE active (no trail blank)"
689 );
690
691 for i in (TEST_COLS - LEAD_BLANK_DELAY)..TEST_COLS {
693 assert_eq!(
694 plane.data[map_index(i)].output_enable(),
695 !oe_active,
696 "plane 0: lead blank entry {i} should have OE blank"
697 );
698 }
699 let active_idx = map_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
700 assert_eq!(
701 plane.data[active_idx].output_enable(),
702 oe_active,
703 "plane 0: entry before the lead blank should have OE active"
704 );
705 }
706 }
707
708 #[test]
709 fn second_plane_has_trail_blank_only() {
710 let fb = TestBuffer::new();
711 let oe_active = !cfg!(feature = "invert-oe");
712
713 for row in &fb.rows {
714 let plane = &row.planes[1];
715
716 for i in 0..TRAIL_BLANK_DELAY {
718 assert_eq!(
719 plane.data[map_index(i)].output_enable(),
720 !oe_active,
721 "plane 1: trail blank entry {i} should have OE blank"
722 );
723 }
724 let active_idx = map_index(TRAIL_BLANK_DELAY);
725 assert_eq!(
726 plane.data[active_idx].output_enable(),
727 oe_active,
728 "plane 1: entry after the trail blank should have OE active"
729 );
730
731 let last_idx = map_index(TEST_COLS - 1);
734 assert_eq!(
735 plane.data[last_idx].output_enable(),
736 oe_active,
737 "plane 1: last pixel should have OE active (no lead blank)"
738 );
739 }
740 }
741
742 #[test]
743 fn middle_planes_have_full_width_window() {
744 let fb = TestBuffer::new();
745 let oe_active = !cfg!(feature = "invert-oe");
746
747 for row in &fb.rows {
750 for plane in row.planes.iter().take(TEST_PLANES - 1).skip(2) {
751 let first_idx = map_index(0);
752 assert_eq!(
753 plane.data[first_idx].output_enable(),
754 oe_active,
755 "middle plane: first pixel should have OE active"
756 );
757 let last_idx = map_index(TEST_COLS - 1);
758 assert_eq!(
759 plane.data[last_idx].output_enable(),
760 oe_active,
761 "middle plane: last pixel should have OE active"
762 );
763 }
764 }
765 }
766
767 #[test]
768 fn last_plane_has_full_width_window() {
769 let fb = TestBuffer::new();
770 let oe_active = !cfg!(feature = "invert-oe");
771 let last = TEST_PLANES - 1;
772
773 for row in &fb.rows {
774 let plane = &row.planes[last];
775
776 let last_idx = map_index(TEST_COLS - 1);
780 assert_eq!(
781 plane.data[last_idx].output_enable(),
782 oe_active,
783 "last plane: last pixel should have OE active (no lead blank)"
784 );
785
786 let first_idx = map_index(0);
788 assert_eq!(
789 plane.data[first_idx].output_enable(),
790 oe_active,
791 "last plane: first pixel should have OE active (no trail blank)"
792 );
793 }
794 }
795
796 #[test]
797 fn oe_window_sizes_match_template() {
798 let fb = TestBuffer::new();
799 let oe_active = !cfg!(feature = "invert-oe");
800
801 for row in &fb.rows {
802 for (p, plane) in row.planes.iter().enumerate() {
803 let trail = if p == 1 { TRAIL_BLANK_DELAY } else { 0 };
804 let lead = if p == 0 { LEAD_BLANK_DELAY } else { 0 };
805 let expected_active = TEST_COLS.saturating_sub(trail + lead);
806 let active = plane
807 .data
808 .iter()
809 .filter(|e| e.output_enable() == oe_active)
810 .count();
811 assert_eq!(active, expected_active, "plane {p} active window size");
812 }
813 }
814 }
815
816 #[test]
817 fn single_plane_has_trail_and_lead_blank() {
818 type OnePlane = DmaFrameBuffer<16, TEST_COLS, 1>;
819 let fb = OnePlane::new();
820
821 for slot in 0..16 {
822 let (_, addr) = slot_addresses::<16>(slot);
823 let plane = &fb.rows[slot].planes[0];
824 assert_eq!(
825 plane.address[map_index(0)].addr(),
826 addr,
827 "single plane slot {slot}: should use the current row address"
828 );
829
830 let expected = make_row_plane_template::<TEST_COLS>(true, true);
833 assert_eq!(
834 plane.data, expected,
835 "single plane slot {slot}: template mismatch"
836 );
837 }
838 }
839
840 #[test]
841 fn set_pixel_maps_top_half_bits_per_plane() {
842 let mut fb = TestBuffer::new();
843 let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
844 fb.set_pixel(Point::new(2, 3), color);
845
846 for plane_idx in 0..TEST_PLANES {
847 let bit = 8 - TEST_PLANES + plane_idx;
848 let entry = fb.rows[map_row_index::<16>(3)].planes[plane_idx].data[map_index(2)];
849 assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
850 assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
851 assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
852 }
853 }
854
855 #[test]
856 fn set_pixel_maps_bottom_half_bits_per_plane() {
857 let mut fb = TestBuffer::new();
858 let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
859 fb.set_pixel(Point::new(4, 20), color);
860
861 for plane_idx in 0..TEST_PLANES {
862 let bit = 8 - TEST_PLANES + plane_idx;
863 let entry = fb.rows[map_row_index::<16>(4)].planes[plane_idx].data[map_index(4)];
864 assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
865 assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
866 assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
867 }
868 }
869
870 #[test]
871 #[cfg(feature = "reverse-row-order")]
872 fn reverse_row_order_stores_rows_back_to_front() {
873 let mut fb = TestBuffer::new();
874
875 assert_eq!(fb.rows[0].planes[0].address[map_index(0)].addr(), 15);
879 assert_eq!(fb.rows[15].planes[0].address[map_index(0)].addr(), 0);
880
881 fb.set_pixel(Point::new(2, 0), Color::RED);
883 let col2 = map_index(2);
884 assert!(fb.rows[15].planes[TEST_PLANES - 1].data[col2].red1());
885 assert!(!fb.rows[0].planes[TEST_PLANES - 1].data[col2].red1());
886 }
887
888 #[test]
889 fn erase_clears_only_color_bits() {
890 let mut fb = TestBuffer::new();
891 let oe_before =
892 fb.rows[map_row_index::<16>(0)].planes[0].data[map_index(1)].output_enable();
893 let addr_before = fb.rows[map_row_index::<16>(3)].planes[2].address;
894 fb.set_pixel(Point::new(0, 0), Color::WHITE);
895 fb.erase();
896
897 for row in &fb.rows {
898 for plane in &row.planes {
899 for entry in &plane.data {
900 assert!(!entry.red1());
901 assert!(!entry.grn1());
902 assert!(!entry.blu1());
903 assert!(!entry.red2());
904 assert!(!entry.grn2());
905 assert!(!entry.blu2());
906 }
907 }
908 }
909
910 assert_eq!(
911 fb.rows[map_row_index::<16>(0)].planes[0].data[map_index(1)].output_enable(),
912 oe_before
913 );
914 assert_eq!(
915 fb.rows[map_row_index::<16>(3)].planes[2].address,
916 addr_before
917 );
918 }
919
920 #[test]
921 fn draw_target_iter_sets_pixels() {
922 let mut fb = TestBuffer::new();
923 let pixels = [Pixel(Point::new(1, 1), Color::RED)];
924 let result = fb.draw_iter(pixels);
925 assert!(result.is_ok());
926
927 for plane_idx in 0..TEST_PLANES {
928 let bit = 8 - TEST_PLANES + plane_idx;
929 let entry = fb.rows[map_row_index::<16>(1)].planes[plane_idx].data[map_index(1)];
930 assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
931 assert!(!entry.grn1());
932 assert!(!entry.blu1());
933 }
934 }
935
936 #[test]
937 fn set_pixel_ignores_out_of_bounds_and_negative() {
938 let mut fb = TestBuffer::new();
939 let before = fb.rows;
940 fb.set_pixel(Point::new(-1, 0), Color::WHITE);
941 fb.set_pixel(Point::new(0, -1), Color::WHITE);
942 fb.set_pixel(Point::new(TEST_COLS as i32, 0), Color::WHITE);
943 fb.set_pixel(Point::new(0, 32), Color::WHITE);
944 assert_eq!(fb.rows, before);
945 }
946
947 #[test]
948 #[cfg(feature = "skip-black-pixels")]
949 fn test_skip_black_pixels_enabled() {
950 let mut fb = TestBuffer::new();
951
952 fb.set_pixel_internal(10, 5, Color::RED);
954
955 let mapped_col_10 = map_index(10);
957 assert!(fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
958 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
959 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
960
961 fb.set_pixel_internal(10, 5, Color::BLACK);
963
964 assert!(fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
966 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
967 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
968 }
969
970 #[test]
971 #[cfg(not(feature = "skip-black-pixels"))]
972 fn test_skip_black_pixels_disabled() {
973 let mut fb = TestBuffer::new();
974
975 fb.set_pixel_internal(10, 5, Color::RED);
977
978 let mapped_col_10 = map_index(10);
980 assert!(fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
981 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
982 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
983
984 fb.set_pixel_internal(10, 5, Color::BLACK);
986
987 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
989 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
990 assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
991 }
992
993 #[test]
994 #[should_panic(expected = "NROWS must be within 1..=32")]
995 fn new_panics_for_too_many_rows() {
996 let _ = DmaFrameBuffer::<33, TEST_COLS, 8>::new();
997 }
998
999 #[test]
1000 #[should_panic(expected = "PLANES must be within 1..=8")]
1001 fn new_panics_for_too_many_planes() {
1002 let _ = DmaFrameBuffer::<16, TEST_COLS, 9>::new();
1003 }
1004
1005 #[test]
1006 fn pixel_row_ptr_len_returns_correct_pointers() {
1007 let fb = TestBuffer::new();
1008 let (ptr, len) = fb.pixel_row_ptr_len(0, 0);
1009 assert_eq!(len, core::mem::size_of::<PlaneRow<TEST_COLS>>());
1010 assert_eq!(ptr, (&raw const fb.rows[0].planes[0]).cast::<u8>());
1011 }
1012
1013 #[test]
1014 fn gap_ptr_len_returns_correct_pointers() {
1015 let fb = TestBuffer::new();
1016 let (ptr, len) = fb.gap_ptr_len(0);
1017 assert_eq!(len, GAP_BYTES);
1018 assert_eq!(ptr, (&raw const fb.rows[0].gap).cast::<u8>());
1019 }
1020
1021 #[test]
1022 #[should_panic(expected = "out of range")]
1023 fn pixel_row_ptr_len_panics_for_invalid_row() {
1024 let fb = TestBuffer::new();
1025 let _ = fb.pixel_row_ptr_len(16, 0);
1026 }
1027
1028 #[test]
1029 #[should_panic(expected = "out of range")]
1030 fn pixel_row_ptr_len_panics_for_invalid_plane() {
1031 let fb = TestBuffer::new();
1032 let _ = fb.pixel_row_ptr_len(0, TEST_PLANES);
1033 }
1034
1035 #[test]
1036 #[should_panic(expected = "out of range")]
1037 fn gap_ptr_len_panics_for_invalid_row() {
1038 let fb = TestBuffer::new();
1039 let _ = fb.gap_ptr_len(16);
1040 }
1041
1042 #[test]
1043 fn origin_dimensions_match_panel_geometry() {
1044 let fb = TestBuffer::new();
1045 assert_eq!(fb.size(), Size::new(TEST_COLS as u32, 32));
1046 }
1047
1048 #[test]
1049 fn default_matches_new() {
1050 let fb_default = TestBuffer::default();
1051 let fb_new = TestBuffer::new();
1052 assert_eq!(fb_default.rows, fb_new.rows);
1053 }
1054
1055 #[test]
1056 fn debug_impl_includes_shape_information() {
1057 let fb = TestBuffer::new();
1058 let s = format!("{fb:?}");
1059 assert!(s.contains("DmaFrameBuffer"));
1060 assert!(s.contains("row_count"));
1061 assert!(s.contains("planes"));
1062 }
1063
1064 static STATIC_FB: TestBuffer = TestBuffer::new();
1065
1066 #[test]
1067 fn static_construction_is_formatted() {
1068 let runtime_fb = TestBuffer::new();
1069
1070 for (ri, row) in STATIC_FB.rows.iter().enumerate() {
1071 assert_eq!(
1072 row.planes, runtime_fb.rows[ri].planes,
1073 "static vs runtime plane mismatch at row {ri}"
1074 );
1075 assert_eq!(
1076 row.gap, runtime_fb.rows[ri].gap,
1077 "static vs runtime gap mismatch at row {ri}"
1078 );
1079 }
1080 }
1081
1082 #[test]
1083 fn format_reinitializes_at_runtime() {
1084 let mut fb = TestBuffer::new();
1085 fb.erase();
1086 fb.format();
1087
1088 for (ri, row) in fb.rows.iter().enumerate() {
1089 assert_eq!(
1090 row.planes, STATIC_FB.rows[ri].planes,
1091 "re-formatted vs static plane mismatch at row {ri}"
1092 );
1093 assert_eq!(
1094 row.gap, STATIC_FB.rows[ri].gap,
1095 "re-formatted vs static gap mismatch at row {ri}"
1096 );
1097 }
1098 }
1099
1100 #[test]
1101 fn framebuffer_operations_trait_delegates_correctly() {
1102 let mut fb = TestBuffer::new();
1103 FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
1104
1105 assert!(fb.rows[map_row_index::<16>(5)].planes[TEST_PLANES - 1].data[map_index(3)].grn1());
1107
1108 FrameBufferOperations::erase(&mut fb);
1109 for row in &fb.rows {
1110 for plane in &row.planes {
1111 for entry in &plane.data {
1112 assert!(!entry.red1());
1113 assert!(!entry.grn1());
1114 assert!(!entry.blu1());
1115 assert!(!entry.red2());
1116 assert!(!entry.grn2());
1117 assert!(!entry.blu2());
1118 }
1119 }
1120 }
1121 }
1122
1123 #[test]
1124 fn row_data_size_matches_expected_layout() {
1125 let expected_planes = core::mem::size_of::<PlaneRow<TEST_COLS>>() * TEST_PLANES;
1126 assert_eq!(
1127 core::mem::size_of::<RowData<TEST_COLS, TEST_PLANES>>(),
1128 expected_planes + GAP_BYTES
1129 );
1130 }
1131
1132 #[test]
1133 fn bcm_segment_count_equals_rows_times_planes_plus_gap() {
1134 let fb = TestBuffer::new();
1135 let gap = if HAS_GAP { 1 } else { 0 };
1136 assert_eq!(fb.bcm_segment_count(), 16 * (TEST_PLANES + gap));
1137 }
1138
1139 #[test]
1140 fn bcm_segments_per_group_equals_planes_plus_gap() {
1141 let fb = TestBuffer::new();
1142 let gap = if HAS_GAP { 1 } else { 0 };
1143 assert_eq!(fb.bcm_segments_per_group(), TEST_PLANES + gap);
1144 assert_eq!(
1145 fb.bcm_segment_count() % fb.bcm_segments_per_group(),
1146 0,
1147 "segment count must be divisible by segments_per_group"
1148 );
1149 }
1150
1151 #[test]
1152 fn bcm_segments_interleave_pixel_and_gap() {
1153 let fb = TestBuffer::new();
1154 let gap = if HAS_GAP { 1 } else { 0 };
1155 let segments_per_row = TEST_PLANES + gap;
1156
1157 for row in 0..16usize {
1158 let seg = fb.bcm_segment(row * segments_per_row);
1160 let (ptr, plane_len) = fb.pixel_row_ptr_len(row, 0);
1161 let covered0 = if HAS_GAP { 1 } else { TEST_PLANES };
1162 assert_eq!(seg.ptr, ptr, "wrong ptr at row {row} plane 0");
1163 assert_eq!(
1164 seg.len,
1165 plane_len * covered0,
1166 "wrong len at row {row} plane 0"
1167 );
1168 assert_eq!(seg.reps, 1, "wrong reps at row {row} plane 0");
1169
1170 if HAS_GAP {
1172 let seg = fb.bcm_segment(row * segments_per_row + 1);
1173 let (ptr, len) = fb.gap_ptr_len(row);
1174 assert_eq!(seg.ptr, ptr, "wrong gap ptr at row {row}");
1175 assert_eq!(seg.len, len, "wrong gap len at row {row}");
1176 assert_eq!(seg.reps, 1, "gap reps must be 1 at row {row}");
1177 }
1178
1179 for plane in 1..TEST_PLANES {
1181 let idx = row * segments_per_row + plane + gap;
1182 let seg = fb.bcm_segment(idx);
1183 let (ptr, plane_len) = fb.pixel_row_ptr_len(row, plane);
1184 let expected_reps = if HAS_GAP && plane == 1 {
1185 2
1186 } else {
1187 1 << (plane - 1)
1188 };
1189 assert_eq!(seg.ptr, ptr, "wrong ptr at row {row} plane {plane}");
1190 assert_eq!(
1191 seg.len,
1192 plane_len * (TEST_PLANES - plane),
1193 "wrong len at row {row} plane {plane}"
1194 );
1195 assert_eq!(
1196 seg.reps, expected_reps,
1197 "wrong reps at row {row} plane {plane}"
1198 );
1199 }
1200 }
1201 }
1202
1203 #[test]
1204 fn bcm_segment_total_reps_per_row() {
1205 let fb = TestBuffer::new();
1206 let gap = if HAS_GAP { 1 } else { 0 };
1207 let segments_per_row = TEST_PLANES + gap;
1208
1209 for row in 0..16usize {
1210 let mut pixel_reps = 0;
1214 for plane in 0..TEST_PLANES {
1215 let offset = if plane == 0 { 0 } else { plane + gap };
1217 pixel_reps += fb.bcm_segment(row * segments_per_row + offset).reps;
1218 }
1219 let expected = if HAS_GAP {
1220 (1 << (TEST_PLANES - 1)) + 1
1221 } else {
1222 1 << (TEST_PLANES - 1)
1223 };
1224 assert_eq!(pixel_reps, expected, "total pixel reps wrong for row {row}");
1225 }
1226 }
1227
1228 #[test]
1229 fn bcm_segment_plane_coverage_matches_bcm_weights() {
1230 let fb = TestBuffer::new();
1231 let gap = if HAS_GAP { 1 } else { 0 };
1232 let segments_per_row = TEST_PLANES + gap;
1233 let plane_bytes = core::mem::size_of::<PlaneRow<TEST_COLS>>();
1234
1235 for row in 0..16usize {
1236 for plane in 0..TEST_PLANES {
1237 let mut coverage = 0;
1241 for first in 0..=plane {
1242 let offset = if first == 0 { 0 } else { first + gap };
1244 let seg = fb.bcm_segment(row * segments_per_row + offset);
1245 if first + seg.len / plane_bytes > plane {
1246 coverage += seg.reps;
1247 }
1248 }
1249 assert_eq!(
1250 coverage,
1251 1 << plane,
1252 "plane {plane} coverage wrong for row {row}"
1253 );
1254 }
1255 }
1256 }
1257
1258 #[test]
1259 #[should_panic(expected = "out of range")]
1260 fn bcm_segment_panics_for_invalid_index() {
1261 let fb = TestBuffer::new();
1262 let gap = if HAS_GAP { 1 } else { 0 };
1263 let segments_per_row = TEST_PLANES + gap;
1264 let _ = fb.bcm_segment(16 * segments_per_row);
1265 }
1266
1267 #[test]
1268 fn bcm_sequence_matches_runtime_segments() {
1269 use crate::FrameBuffer;
1270 let fb = TestBuffer::new();
1271 assert_eq!(
1272 TestBuffer::BCM_SEGMENT_COUNT,
1273 TestBuffer::BCM_SEQUENCE_LEN * TestBuffer::BCM_SEQUENCE_COUNT
1274 );
1275 assert_eq!(fb.bcm_segment_count(), TestBuffer::BCM_SEGMENT_COUNT);
1276 assert_eq!(
1277 fb.bcm_segments_per_group(),
1278 TestBuffer::BCM_SEGMENTS_PER_GROUP
1279 );
1280 assert_eq!(
1281 TestBuffer::BCM_SEQUENCE_LEN % TestBuffer::BCM_SEGMENTS_PER_GROUP,
1282 0
1283 );
1284 for i in 0..TestBuffer::BCM_SEGMENT_COUNT {
1285 let entry = TestBuffer::BCM_SEQUENCE[i % TestBuffer::BCM_SEQUENCE_LEN];
1286 let seg = fb.bcm_segment(i);
1287 assert_eq!((seg.len, seg.reps), (entry.len, entry.reps), "segment {i}");
1288 assert!(!seg.ptr.is_null(), "segment {i} has null pointer");
1289 }
1290 for entry in &TestBuffer::BCM_SEQUENCE[TestBuffer::BCM_SEQUENCE_LEN..] {
1291 assert_eq!(*entry, crate::BcmLenReps::ZERO, "padding must be zero");
1292 }
1293 }
1294
1295 #[test]
1296 fn bcm_segment_data_contains_written_pixels() {
1297 const TRINITY_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
1298 128
1299 } else {
1300 64
1301 };
1302 type TrinityBuf = DmaFrameBuffer<32, TRINITY_COLS, 5>;
1303 let mut fb = TrinityBuf::new();
1304
1305 fb.set_pixel(Point::new(10, 0), Color::WHITE);
1306
1307 let col_idx = map_index(10);
1308 let gap = usize::from(HAS_GAP);
1309 let row0_base = map_row_index::<32>(0) * (5 + gap);
1311
1312 for plane_idx in 0..5usize {
1313 let seg = fb.bcm_segment(if plane_idx == 0 {
1316 row0_base
1317 } else {
1318 row0_base + plane_idx + gap
1319 });
1320
1321 let data = unsafe { core::slice::from_raw_parts(seg.ptr, TRINITY_COLS) };
1322 let raw = data[col_idx];
1323 let entry = Entry(raw);
1324
1325 let bit = 8 - 5 + plane_idx; let expect_r = (0xFF >> bit) & 1 != 0;
1327 let expect_g = (0xFF >> bit) & 1 != 0;
1328 let expect_b = (0xFF >> bit) & 1 != 0;
1329 assert_eq!(
1330 entry.red1(),
1331 expect_r,
1332 "plane {plane_idx} (bit {bit}) R1 mismatch: raw={raw:#04x}"
1333 );
1334 assert_eq!(
1335 entry.grn1(),
1336 expect_g,
1337 "plane {plane_idx} (bit {bit}) G1 mismatch: raw={raw:#04x}"
1338 );
1339 assert_eq!(
1340 entry.blu1(),
1341 expect_b,
1342 "plane {plane_idx} (bit {bit}) B1 mismatch: raw={raw:#04x}"
1343 );
1344 }
1345 }
1346
1347 #[test]
1348 fn bcm_segment_data_contains_gradient_pixels() {
1349 const TRINITY_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
1350 128
1351 } else {
1352 64
1353 };
1354 type TrinityBuf = DmaFrameBuffer<32, TRINITY_COLS, 5>;
1355 let mut fb = TrinityBuf::new();
1356
1357 let step: u8 = (256 / TRINITY_COLS) as u8;
1358 for x in 0..TRINITY_COLS {
1359 let brightness = (x as u8).wrapping_mul(step);
1360 fb.set_pixel(Point::new(x as i32, 0), Color::new(brightness, 0, 0));
1361 }
1362
1363 let mut any_nonzero = false;
1364 let gap = usize::from(HAS_GAP);
1365 let row0_base = map_row_index::<32>(0) * (5 + gap);
1367 for plane_idx in 0..5usize {
1368 let seg = fb.bcm_segment(if plane_idx == 0 {
1371 row0_base
1372 } else {
1373 row0_base + plane_idx + gap
1374 });
1375 let data = unsafe { core::slice::from_raw_parts(seg.ptr, TRINITY_COLS) };
1376
1377 for x in 0..TRINITY_COLS {
1378 let col_idx = map_index(x);
1379 let entry = Entry(data[col_idx]);
1380 let brightness = (x as u8).wrapping_mul(step);
1381 let bit = 8 - 5 + plane_idx; let expect_r = (brightness >> bit) & 1 != 0;
1383 assert_eq!(
1384 entry.red1(),
1385 expect_r,
1386 "x={x} plane={plane_idx} bit={bit} brightness={brightness} \
1387 raw={:#04x}",
1388 data[col_idx]
1389 );
1390 if expect_r {
1391 any_nonzero = true;
1392 }
1393 }
1394 }
1395 assert!(
1396 any_nonzero,
1397 "gradient should produce at least some non-zero color bits"
1398 );
1399 }
1400}