1use core::convert::Infallible;
71
72use bitfield::bitfield;
73use embedded_graphics::pixelcolor::RgbColor;
74use embedded_graphics::prelude::{DrawTarget, OriginDimensions, Point, Size};
75
76use crate::Color;
77use crate::FrameBuffer;
78use crate::{FrameBufferOperations, MutableFrameBuffer};
79
80#[cfg(feature = "lead-blank-1")]
81const LEAD_BLANK_DELAY: usize = 1;
82#[cfg(feature = "lead-blank-2")]
83const LEAD_BLANK_DELAY: usize = 2;
84#[cfg(feature = "lead-blank-4")]
85const LEAD_BLANK_DELAY: usize = 4;
86#[cfg(feature = "lead-blank-8")]
87const LEAD_BLANK_DELAY: usize = 8;
88#[cfg(feature = "lead-blank-16")]
89const LEAD_BLANK_DELAY: usize = 16;
90
91#[cfg(not(any(
92 feature = "lead-blank-1",
93 feature = "lead-blank-2",
94 feature = "lead-blank-4",
95 feature = "lead-blank-8",
96 feature = "lead-blank-16"
97)))]
98const LEAD_BLANK_DELAY: usize = 0;
99
100#[cfg(feature = "trail-blank-1")]
101const TRAIL_BLANK_DELAY: usize = 1;
102#[cfg(feature = "trail-blank-2")]
103const TRAIL_BLANK_DELAY: usize = 2;
104#[cfg(feature = "trail-blank-4")]
105const TRAIL_BLANK_DELAY: usize = 4;
106#[cfg(feature = "trail-blank-8")]
107const TRAIL_BLANK_DELAY: usize = 8;
108#[cfg(feature = "trail-blank-16")]
109const TRAIL_BLANK_DELAY: usize = 16;
110
111#[cfg(not(any(
112 feature = "trail-blank-1",
113 feature = "trail-blank-2",
114 feature = "trail-blank-4",
115 feature = "trail-blank-8",
116 feature = "trail-blank-16"
117)))]
118const TRAIL_BLANK_DELAY: usize = 0;
119
120#[cfg(feature = "inter-row-blank-4")]
121const INTER_ROW_BLANK: usize = 4;
122#[cfg(feature = "inter-row-blank-8")]
123const INTER_ROW_BLANK: usize = 8;
124#[cfg(feature = "inter-row-blank-16")]
125const INTER_ROW_BLANK: usize = 16;
126#[cfg(feature = "inter-row-blank-32")]
127const INTER_ROW_BLANK: usize = 32;
128#[cfg(not(any(
129 feature = "inter-row-blank-4",
130 feature = "inter-row-blank-8",
131 feature = "inter-row-blank-16",
132 feature = "inter-row-blank-32"
133)))]
134const INTER_ROW_BLANK: usize = 0;
135
136#[cfg(not(feature = "invert-oe"))]
137const OE_ACTIVE: u8 = 0b1000_0000;
138#[cfg(not(feature = "invert-oe"))]
139const OE_BLANK: u8 = 0;
140
141#[cfg(feature = "invert-oe")]
142const OE_ACTIVE: u8 = 0;
143#[cfg(feature = "invert-oe")]
144const OE_BLANK: u8 = 0b1000_0000;
145
146bitfield! {
147 #[derive(Clone, Copy, Default, PartialEq, Eq)]
148 #[repr(transparent)]
149 pub(crate) struct Address(u8);
150 impl Debug;
151 pub(crate) output_enable, set_output_enable: 7;
152 pub(crate) latch, set_latch: 6;
153 pub(crate) addr, set_addr: 4, 0;
154}
155
156impl Address {
157 pub const fn new() -> Self {
158 Self(0)
159 }
160}
161
162bitfield! {
163 #[derive(Clone, Copy, Default, PartialEq)]
164 #[repr(transparent)]
165 pub(crate) struct Entry(u8);
166 impl Debug;
167 pub(crate) output_enable, set_output_enable: 7;
168 pub(crate) latch, set_latch: 6;
169 pub(crate) blu2, set_blu2: 5;
170 pub(crate) grn2, set_grn2: 4;
171 pub(crate) red2, set_red2: 3;
172 pub(crate) blu1, set_blu1: 2;
173 pub(crate) grn1, set_grn1: 1;
174 pub(crate) red1, set_red1: 0;
175}
176
177impl Entry {
178 pub const fn new() -> Self {
179 Self(0)
180 }
181
182 const COLOR0_MASK: u8 = 0b0000_0111;
183 const COLOR1_MASK: u8 = 0b0011_1000;
184
185 #[inline]
186 fn set_color0_bits(&mut self, bits: u8) {
187 self.0 = (self.0 & !Self::COLOR0_MASK) | (bits & Self::COLOR0_MASK);
188 }
189
190 #[inline]
191 fn set_color1_bits(&mut self, bits: u8) {
192 self.0 = (self.0 & !Self::COLOR1_MASK) | ((bits << 3) & Self::COLOR1_MASK);
193 }
194}
195
196#[derive(Clone, Copy, PartialEq, Debug)]
197#[repr(C)]
198pub struct Row<const COLS: usize> {
203 pub(crate) data: [Entry; COLS],
204 pub(crate) address: [Address; 4],
205 pub(crate) gap: [Entry; INTER_ROW_BLANK],
206}
207
208#[inline]
209const fn map_index(index: usize) -> usize {
210 #[cfg(feature = "esp32-ordering")]
211 {
212 index ^ 2
213 }
214 #[cfg(not(feature = "esp32-ordering"))]
215 {
216 index
217 }
218}
219
220const fn make_addr_table() -> [[Address; 4]; 32] {
221 let mut tbl = [[Address::new(); 4]; 32];
222 let mut addr = 0;
223 while addr < 32 {
224 tbl[addr][map_index(0)].0 = OE_BLANK | 1u8 << 6 | addr as u8;
225 tbl[addr][map_index(1)].0 = OE_BLANK | 1u8 << 6 | addr as u8;
226 tbl[addr][map_index(2)].0 = OE_BLANK | addr as u8;
227 tbl[addr][map_index(3)].0 = OE_BLANK;
228 addr += 1;
229 }
230 tbl
231}
232
233const ADDR_TABLE: [[Address; 4]; 32] = make_addr_table();
234
235#[allow(clippy::absurd_extreme_comparisons)]
236const fn make_data_template<const COLS: usize>() -> [Entry; COLS] {
237 let mut data = [Entry::new(); COLS];
238 let mut i = 0;
239 while i < COLS {
240 let mapped_i = map_index(i);
241 data[mapped_i].0 =
242 if i >= TRAIL_BLANK_DELAY && i < COLS.saturating_sub(LEAD_BLANK_DELAY + 1) {
243 OE_ACTIVE
244 } else {
245 OE_BLANK
246 };
247 i += 1;
248 }
249 data
250}
251
252impl<const COLS: usize> Row<COLS> {
253 #[must_use]
258 pub const fn new() -> Self {
259 Self {
260 data: [Entry::new(); COLS],
261 address: [Address::new(); 4],
262 gap: [Entry::new(); INTER_ROW_BLANK],
263 }
264 }
265
266 #[inline]
275 pub const fn format(&mut self, addr: u8) {
276 assert!((addr as usize) < ADDR_TABLE.len());
277 let src_addr = ADDR_TABLE[addr as usize];
278 self.address[0] = src_addr[0];
279 self.address[1] = src_addr[1];
280 self.address[2] = src_addr[2];
281 self.address[3] = src_addr[3];
282
283 let data_template = make_data_template::<COLS>();
284 let mut i = 0;
285 while i < COLS {
286 self.data[i] = data_template[i];
287 i += 1;
288 }
289
290 let mut i = 0;
291 #[allow(clippy::absurd_extreme_comparisons)]
293 while i < INTER_ROW_BLANK {
294 self.gap[i].0 = OE_BLANK;
295 i += 1;
296 }
297 }
298}
299
300impl<const COLS: usize> Default for Row<COLS> {
301 fn default() -> Self {
302 Self::new()
303 }
304}
305
306#[derive(Copy, Clone)]
308#[repr(C)]
309pub struct DmaFrameBuffer<const NROWS: usize, const COLS: usize, const PLANES: usize> {
310 pub(crate) planes: [[Row<COLS>; NROWS]; PLANES],
311}
312
313impl<const NROWS: usize, const COLS: usize, const PLANES: usize>
314 DmaFrameBuffer<NROWS, COLS, PLANES>
315{
316 #[must_use]
318 pub const fn new() -> Self {
319 let mut instance = Self {
320 planes: [[Row::new(); NROWS]; PLANES],
321 };
322 instance.format();
323 instance
324 }
325
326 #[must_use]
328 pub const fn bcm_chunk_count() -> usize {
329 PLANES
330 }
331
332 #[must_use]
334 pub const fn bcm_chunk_bytes() -> usize {
335 NROWS * core::mem::size_of::<Row<COLS>>()
336 }
337
338 #[must_use]
340 pub const fn bcm_row_count() -> usize {
341 NROWS
342 }
343
344 #[inline]
346 pub const fn format(&mut self) {
347 let mut p = 0;
348 while p < PLANES {
349 let mut row_idx = 0;
350 while row_idx < NROWS {
351 self.planes[p][row_idx].format(row_idx as u8);
352 row_idx += 1;
353 }
354 p += 1;
355 }
356 }
357
358 #[inline]
360 pub fn erase(&mut self) {
361 const MASK: u8 = !0b0011_1111;
362 for plane in &mut self.planes {
363 for row in plane {
364 for entry in &mut row.data {
365 entry.0 &= MASK;
366 }
367 }
368 }
369 }
370
371 #[inline]
373 pub fn set_pixel(&mut self, p: Point, color: Color) {
374 if p.x < 0 || p.y < 0 {
375 return;
376 }
377 self.set_pixel_internal(p.x as usize, p.y as usize, color);
378 }
379
380 #[inline]
381 fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
382 if x >= COLS || y >= NROWS * 2 {
383 return;
384 }
385
386 let row_idx = if y < NROWS { y } else { y - NROWS };
387 let is_top = y < NROWS;
388 let red = color.r();
389 let green = color.g();
390 let blue = color.b();
391
392 for plane_idx in 0..PLANES {
393 let bit = 7_u32.saturating_sub(plane_idx as u32);
394 let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
395 | ((u8::from(((green >> bit) & 1) != 0)) << 1)
396 | u8::from(((red >> bit) & 1) != 0);
397 let col_idx = map_index(x);
398 let entry = &mut self.planes[plane_idx][row_idx].data[col_idx];
399 if is_top {
400 entry.set_color0_bits(bits);
401 } else {
402 entry.set_color1_bits(bits);
403 }
404 }
405 }
406}
407
408impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
409 for DmaFrameBuffer<NROWS, COLS, PLANES>
410{
411 fn default() -> Self {
412 Self::new()
413 }
414}
415
416impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
417 for DmaFrameBuffer<NROWS, COLS, PLANES>
418{
419 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
420 f.debug_struct("DmaFrameBuffer")
421 .field("size", &core::mem::size_of_val(&self.planes))
422 .field("plane_count", &self.planes.len())
423 .field("plane_size", &core::mem::size_of_val(&self.planes[0]))
424 .finish()
425 }
426}
427
428#[cfg(feature = "defmt")]
429impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
430 for DmaFrameBuffer<NROWS, COLS, PLANES>
431{
432 fn format(&self, f: defmt::Formatter) {
433 defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
434 defmt::write!(f, " size: {}", core::mem::size_of_val(&self.planes));
435 defmt::write!(
436 f,
437 " plane_size: {}",
438 core::mem::size_of_val(&self.planes[0])
439 );
440 }
441}
442
443impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
444 for DmaFrameBuffer<NROWS, COLS, PLANES>
445{
446 type Word = u8;
447
448 fn plane_count(&self) -> usize {
449 PLANES
450 }
451
452 fn plane_ptr_len(&self, plane_idx: usize) -> (*const u8, usize) {
453 assert!(
454 plane_idx < PLANES,
455 "plane_idx {plane_idx} out of range for {PLANES} planes"
456 );
457 let ptr = self.planes[plane_idx].as_ptr().cast::<u8>();
458 let len = NROWS * core::mem::size_of::<Row<COLS>>();
459 (ptr, len)
460 }
461}
462
463impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
464 for DmaFrameBuffer<NROWS, COLS, PLANES>
465{
466 #[inline]
467 fn erase(&mut self) {
468 DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
469 }
470
471 #[inline]
472 fn set_pixel(&mut self, p: Point, color: Color) {
473 DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
474 }
475}
476
477impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
478 for DmaFrameBuffer<NROWS, COLS, PLANES>
479{
480}
481
482impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
483 for DmaFrameBuffer<NROWS, COLS, PLANES>
484{
485 fn size(&self) -> Size {
486 Size::new(COLS as u32, (NROWS * 2) as u32)
487 }
488}
489
490impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
491 for DmaFrameBuffer<NROWS, COLS, PLANES>
492{
493 type Color = Color;
494 type Error = Infallible;
495
496 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
497 where
498 I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
499 {
500 for pixel in pixels {
501 self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
502 }
503 Ok(())
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 extern crate std;
510
511 use super::*;
512 use embedded_graphics::prelude::*;
513 use std::format;
514
515 type TestBuffer = DmaFrameBuffer<16, 64, 8>;
516
517 #[test]
518 fn row_format_sets_address_and_control_bits() {
519 const TEST_N: usize = 64;
520 let mut row = Row::<TEST_N>::new();
521 row.format(5);
522 let latch_count = row.address.iter().filter(|a| a.latch()).count();
523 assert_eq!(latch_count, 2);
524 assert_eq!(row.address[map_index(0)].addr(), 5);
525 assert_eq!(row.address[map_index(1)].addr(), 5);
526 assert_eq!(row.address[map_index(2)].addr(), 5);
527 assert_eq!(row.address[map_index(3)].0, OE_BLANK);
528 let oe_active = !cfg!(feature = "invert-oe");
529 let active_count = TEST_N.saturating_sub(LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 1);
530 let blank_count = TEST_N - active_count;
531 let oe_blank_count = row
532 .data
533 .iter()
534 .filter(|entry| entry.output_enable() != oe_active)
535 .count();
536 assert_eq!(oe_blank_count, blank_count);
537 }
538
539 #[test]
540 fn format_sets_expected_row_addresses_for_all_rows() {
541 let mut fb = TestBuffer::new();
542 fb.format();
543
544 for plane_idx in 0..8 {
545 for row_idx in 0..16 {
546 let row = &fb.planes[plane_idx][row_idx];
547 assert_eq!(row.address[map_index(0)].addr(), row_idx as u8);
548 assert_eq!(row.address[map_index(1)].addr(), row_idx as u8);
549 assert_eq!(row.address[map_index(2)].addr(), row_idx as u8);
550 assert_eq!(row.address[map_index(3)].0, OE_BLANK);
551 }
552 }
553 }
554
555 #[test]
556 fn set_pixel_maps_top_half_bits_per_plane() {
557 let mut fb = TestBuffer::new();
558 let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
559 fb.set_pixel(Point::new(2, 3), color);
560
561 for plane_idx in 0..8 {
562 let bit = 7 - plane_idx;
563 let entry = fb.planes[plane_idx][3].data[map_index(2)];
564 assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
565 assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
566 assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
567 }
568 }
569
570 #[test]
571 fn set_pixel_maps_bottom_half_bits_per_plane() {
572 let mut fb = TestBuffer::new();
573 let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
574 fb.set_pixel(Point::new(4, 20), color);
575
576 for plane_idx in 0..8 {
577 let bit = 7 - plane_idx;
578 let entry = fb.planes[plane_idx][4].data[map_index(4)];
579 assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
580 assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
581 assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
582 }
583 }
584
585 #[test]
586 fn erase_clears_only_color_bits() {
587 let mut fb = TestBuffer::new();
588 let oe_before = fb.planes[0][0].data[0].output_enable();
589 fb.set_pixel(Point::new(0, 0), Color::WHITE);
590 fb.erase();
591
592 for plane in &fb.planes {
593 for row in plane {
594 for entry in &row.data {
595 assert!(!entry.red1());
596 assert!(!entry.grn1());
597 assert!(!entry.blu1());
598 assert!(!entry.red2());
599 assert!(!entry.grn2());
600 assert!(!entry.blu2());
601 }
602 }
603 }
604
605 assert_eq!(fb.planes[0][0].data[0].output_enable(), oe_before);
606 }
607
608 #[test]
609 fn draw_target_iter_sets_pixels() {
610 let mut fb = TestBuffer::new();
611 let pixels = [Pixel(Point::new(1, 1), Color::RED)];
612 let result = fb.draw_iter(pixels);
613 assert!(result.is_ok());
614
615 for plane_idx in 0..8 {
616 let bit = 7 - plane_idx;
617 let entry = fb.planes[plane_idx][1].data[map_index(1)];
618 assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
619 assert!(!entry.grn1());
620 assert!(!entry.blu1());
621 }
622 }
623
624 #[test]
625 fn set_pixel_ignores_out_of_bounds_and_negative() {
626 let mut fb = TestBuffer::new();
627 let before = fb.planes;
628 fb.set_pixel(Point::new(-1, 0), Color::WHITE);
629 fb.set_pixel(Point::new(0, -1), Color::WHITE);
630 fb.set_pixel(Point::new(64, 0), Color::WHITE);
631 fb.set_pixel(Point::new(0, 32), Color::WHITE);
632 assert_eq!(fb.planes, before);
633 }
634
635 #[test]
636 fn bcm_chunk_info_for_common_panel() {
637 assert_eq!(TestBuffer::bcm_chunk_count(), 8);
638 assert_eq!(
639 TestBuffer::bcm_chunk_bytes(),
640 16 * core::mem::size_of::<Row<64>>()
641 );
642 }
643
644 #[test]
645 fn frame_buffer_trait_accessors_report_expected_values() {
646 let fb = TestBuffer::new();
647 let as_trait: &dyn FrameBuffer<Word = u8> = &fb;
648 assert_eq!(as_trait.get_word_size(), crate::WordSize::Eight);
649 assert_eq!(as_trait.plane_count(), 8);
650
651 let (ptr, len) = as_trait.plane_ptr_len(0);
652 assert_eq!(len, 16 * core::mem::size_of::<Row<64>>());
653 assert_eq!(ptr, fb.planes[0].as_ptr().cast::<u8>());
654 }
655
656 #[test]
657 #[should_panic(expected = "out of range")]
658 fn plane_ptr_len_panics_for_invalid_plane() {
659 let fb = TestBuffer::new();
660 let _ = fb.plane_ptr_len(8);
661 }
662
663 #[test]
664 fn origin_dimensions_match_panel_geometry() {
665 let fb = TestBuffer::new();
666 assert_eq!(fb.size(), Size::new(64, 32));
667 }
668
669 #[test]
670 fn debug_impl_includes_shape_information() {
671 let fb = TestBuffer::new();
672 let s = format!("{fb:?}");
673 assert!(s.contains("DmaFrameBuffer"));
674 assert!(s.contains("plane_count"));
675 assert!(s.contains("plane_size"));
676 }
677
678 #[test]
679 fn row_format_sets_exactly_one_data_word_with_oe_low() {
680 const TEST_N: usize = 64;
681 let mut row = Row::<TEST_N>::new();
682 row.format(9);
683
684 let oe_active = !cfg!(feature = "invert-oe");
685 let active_count = TEST_N.saturating_sub(LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 1);
686 let blank_count = TEST_N - active_count;
687 let oe_blank_indices: std::vec::Vec<_> = row
688 .data
689 .iter()
690 .enumerate()
691 .filter_map(|(i, entry)| (entry.output_enable() != oe_active).then_some(i))
692 .collect();
693 assert_eq!(oe_blank_indices.len(), blank_count);
694 assert!(oe_blank_indices.contains(&map_index(TEST_N - 1)));
695 }
696
697 #[test]
698 fn default_constructors_match_new() {
699 let row_default = Row::<64>::default();
700 let row_new = Row::<64>::new();
701 assert_eq!(row_default, row_new);
702
703 let fb_default = TestBuffer::default();
704 let fb_new = TestBuffer::new();
705 assert_eq!(fb_default.planes, fb_new.planes);
706 }
707
708 #[test]
709 fn framebuffer_operations_trait_delegates_correctly() {
710 let mut fb = TestBuffer::new();
711 FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
712
713 assert!(fb.planes[0][5].data[map_index(3)].grn1());
714
715 FrameBufferOperations::erase(&mut fb);
716 for plane in &fb.planes {
717 for row in plane {
718 for entry in &row.data {
719 assert!(!entry.red1());
720 assert!(!entry.grn1());
721 assert!(!entry.blu1());
722 assert!(!entry.red2());
723 assert!(!entry.grn2());
724 assert!(!entry.blu2());
725 }
726 }
727 }
728 }
729
730 #[test]
731 fn addr_table_entries_are_consistent() {
732 let table = make_addr_table();
733 for addr in 0..32u8 {
734 let row = &table[addr as usize];
735 assert!(row[map_index(0)].latch());
737 assert_eq!(row[map_index(0)].addr(), addr);
738 assert!(row[map_index(1)].latch());
739 assert_eq!(row[map_index(1)].addr(), addr);
740 assert!(!row[map_index(2)].latch());
742 assert_eq!(row[map_index(2)].addr(), addr);
743 assert!(!row[map_index(3)].latch());
745 assert_eq!(row[map_index(3)].0, OE_BLANK);
746 }
747 assert_eq!(table, ADDR_TABLE);
748 }
749
750 #[test]
751 fn test_blanking_delay() {
752 let mut row = Row::<64>::new();
753 row.format(5);
754
755 let oe_active = !cfg!(feature = "invert-oe");
756
757 if TRAIL_BLANK_DELAY > 0 {
759 let trail_blank_idx = map_index(TRAIL_BLANK_DELAY - 1);
760 assert_eq!(row.data[trail_blank_idx].output_enable(), !oe_active);
761 }
762
763 let first_active_idx = map_index(TRAIL_BLANK_DELAY);
764 assert_eq!(row.data[first_active_idx].output_enable(), oe_active);
765
766 let last_active_idx = map_index(64 - LEAD_BLANK_DELAY - 2);
768 assert_eq!(row.data[last_active_idx].output_enable(), oe_active);
769
770 let lead_blank_idx = map_index(64 - LEAD_BLANK_DELAY - 1);
771 assert_eq!(row.data[lead_blank_idx].output_enable(), !oe_active);
772
773 let last_pixel_idx = map_index(63);
774 assert_eq!(row.data[last_pixel_idx].output_enable(), !oe_active);
775 }
776
777 static STATIC_FB: TestBuffer = TestBuffer::new();
778
779 #[test]
780 fn test_static_construction_is_formatted() {
781 let runtime_fb = TestBuffer::new();
782
783 for (pi, plane) in STATIC_FB.planes.iter().enumerate() {
784 for (ri, row) in plane.iter().enumerate() {
785 assert_eq!(
786 row.data, runtime_fb.planes[pi][ri].data,
787 "static vs runtime data mismatch at plane {pi}, row {ri}"
788 );
789 assert_eq!(
790 row.address, runtime_fb.planes[pi][ri].address,
791 "static vs runtime address mismatch at plane {pi}, row {ri}"
792 );
793 assert_eq!(
794 row.gap, runtime_fb.planes[pi][ri].gap,
795 "static vs runtime gap mismatch at plane {pi}, row {ri}"
796 );
797 }
798 }
799 }
800
801 #[test]
802 fn test_format_reinitializes_at_runtime() {
803 let mut fb = TestBuffer::new();
804 fb.erase();
805 fb.format();
806
807 for (pi, plane) in fb.planes.iter().enumerate() {
808 for (ri, row) in plane.iter().enumerate() {
809 assert_eq!(
810 row.data, STATIC_FB.planes[pi][ri].data,
811 "re-formatted vs static mismatch at plane {pi}, row {ri}"
812 );
813 }
814 }
815 }
816}