Skip to main content

hub75_framebuffer/bitplane/
plain.rs

1//! Bitplane framebuffer for a 16-bit plain HUB75 interface.
2//!
3//! This module provides a framebuffer that stores colour data as separate
4//! bit-planes rather than the threshold-based frames used by
5//! [`crate::plain::DmaFrameBuffer`]. Each plane holds one bit of every colour
6//! channel, giving `PLANES` planes total (typically 8 for full 8-bit colour).
7//! Row addressing, latch, OE, and colour data are all packed into each 16-bit
8//! word -- no external latch circuit is required.
9//!
10//! # Hardware Requirements
11//! Requires a parallel output peripheral capable of clocking 16 bits (though
12//! only 15 are actually used) at a time. The data is structured to directly
13//! match the HUB75 connector signals, so no external latch or address-decode
14//! hardware is needed.
15//!
16//! # HUB75 Signal Bit Mapping
17//! Each 16-bit `Entry` represents the logic levels driven onto the HUB75 bus
18//! during a single pixel-clock cycle:
19//!
20//! ```text
21//! 15 ─ (spare)
22//! 14 ─ B2       Blue  – lower half of the panel
23//! 13 ─ G2       Green – lower half of the panel
24//! 12 ─ R2       Red   – lower half of the panel
25//! 11 ─ B1       Blue  – upper half of the panel
26//! 10 ─ G1       Green – upper half of the panel
27//!  9 ─ R1       Red   – upper half of the panel
28//!  8 ─ OE       Output-Enable / Blank
29//!  7 ─ (spare)
30//!  6 ─ (spare)
31//!  5 ─ LAT      Latch / STB
32//! 4-0 ─ A..E    Row address lines
33//! ```
34//!
35//! The pixel clock is generated by the peripheral that owns the DMA stream and
36//! is not part of the 16-bit word stored in the framebuffer.
37//!
38//! # Bitplane BCM Rendering
39//! The framebuffer is organised into `PLANES` bit-planes. Plane 0 carries the
40//! MSB (bit 7) of each colour channel, plane 1 carries bit 6, and so on down
41//! to plane 7 which carries the LSB (bit 0).
42//!
43//! To produce correct brightness via Binary Code Modulation, configure the DMA
44//! descriptor chain so that each plane's data is output (scanned) a number of
45//! times equal to its bit-weight:
46//!
47//! ```text
48//! plane 0 (bit 7) → output 2^7 = 128 times
49//! plane 1 (bit 6) → output 2^6 =  64 times
50//! plane 2 (bit 5) → output 2^5 =  32 times
51//!   …
52//! plane 7 (bit 0) → output 2^0 =   1 time
53//! ```
54//!
55//! That is, each plane is scanned `2^(7 - plane_index)` times. The weighted
56//! repetition counts sum to 255, reproducing the full 8-bit intensity range.
57//! See <https://www.batsocks.co.uk/readme/art_bcm_1.htm> for background on
58//! BCM.
59//!
60//! # Memory Usage
61//! Memory scales linearly with `PLANES`: the buffer contains `PLANES` copies
62//! of the row data (one per bit-plane). Unlike the threshold-based
63//! [`crate::plain::DmaFrameBuffer`] whose frame count grows as
64//! `2^BITS - 1`, this layout uses exactly `PLANES` planes regardless of
65//! colour depth.
66//!
67//! Each row is `COLS` 16-bit entries, so total size is
68//! `PLANES * NROWS * COLS * 2` bytes.
69
70use 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 = "blank-delay-1")]
81const BLANKING_DELAY: usize = 1;
82#[cfg(feature = "blank-delay-2")]
83const BLANKING_DELAY: usize = 2;
84#[cfg(feature = "blank-delay-4")]
85const BLANKING_DELAY: usize = 4;
86#[cfg(feature = "blank-delay-8")]
87const BLANKING_DELAY: usize = 8;
88
89// Default to 1 if no blanking delay feature is enabled
90#[cfg(not(any(
91    feature = "blank-delay-1",
92    feature = "blank-delay-2",
93    feature = "blank-delay-4",
94    feature = "blank-delay-8"
95)))]
96const BLANKING_DELAY: usize = 1;
97
98#[cfg(not(feature = "invert-oe"))]
99const OE_ACTIVE: u16 = 0b1_0000_0000;
100#[cfg(not(feature = "invert-oe"))]
101const OE_BLANK: u16 = 0;
102
103#[cfg(feature = "invert-oe")]
104const OE_ACTIVE: u16 = 0;
105#[cfg(feature = "invert-oe")]
106const OE_BLANK: u16 = 0b1_0000_0000;
107
108#[inline]
109const fn map_index(i: usize) -> usize {
110    #[cfg(feature = "esp32-ordering")]
111    {
112        i ^ 1
113    }
114    #[cfg(not(feature = "esp32-ordering"))]
115    {
116        i
117    }
118}
119
120#[inline]
121const fn make_data_template<const COLS: usize>(addr: u8, prev_addr: u8) -> [Entry; COLS] {
122    let mut data = [Entry::new(); COLS];
123    let mut i = 0;
124
125    while i < COLS {
126        let mut entry = Entry::new();
127        // start with blanking
128        entry.0 = prev_addr as u16 | OE_BLANK;
129
130        if i == COLS - 1 {
131            // last pixel is a latch and new address
132            entry.0 |= 0b0010_0000; // latch
133            entry.0 = (entry.0 & !0b0001_1111) | (addr as u16); // new address
134        } else if i >= BLANKING_DELAY && i < COLS - BLANKING_DELAY - 1 {
135            // active after blanking delay at the start and before blanking delay at the end
136            entry.0 = (entry.0 & !0b1_0000_0000) | OE_ACTIVE;
137        }
138
139        data[map_index(i)] = entry;
140        i += 1;
141    }
142
143    data
144}
145
146bitfield! {
147    #[derive(Clone, Copy, Default, PartialEq)]
148    #[repr(transparent)]
149    pub(crate) struct Entry(u16);
150    pub(crate) dummy2, set_dummy2: 15;
151    pub(crate) blu2, set_blu2: 14;
152    pub(crate) grn2, set_grn2: 13;
153    pub(crate) red2, set_red2: 12;
154    pub(crate) blu1, set_blu1: 11;
155    pub(crate) grn1, set_grn1: 10;
156    pub(crate) red1, set_red1: 9;
157    pub(crate) output_enable, set_output_enable: 8;
158    pub(crate) dummy1, set_dummy1: 7;
159    pub(crate) dummy0, set_dummy0: 6;
160    pub(crate) latch, set_latch: 5;
161    pub(crate) addr, set_addr: 4, 0;
162}
163
164impl core::fmt::Debug for Entry {
165    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
166        f.debug_tuple("Entry")
167            .field(&format_args!("{:#x}", self.0))
168            .finish()
169    }
170}
171
172impl Entry {
173    const fn new() -> Self {
174        Self(OE_BLANK)
175    }
176
177    const COLOR0_MASK: u16 = 0b0000_1110_0000_0000; // bits 9-11: R1, G1, B1
178    const COLOR1_MASK: u16 = 0b0111_0000_0000_0000; // bits 12-14: R2, G2, B2
179
180    #[inline]
181    fn set_color0_bits(&mut self, bits: u8) {
182        let bits16 = u16::from(bits) << 9;
183        self.0 = (self.0 & !Self::COLOR0_MASK) | (bits16 & Self::COLOR0_MASK);
184    }
185
186    #[inline]
187    fn set_color1_bits(&mut self, bits: u8) {
188        let bits16 = u16::from(bits) << 12;
189        self.0 = (self.0 & !Self::COLOR1_MASK) | (bits16 & Self::COLOR1_MASK);
190    }
191}
192
193#[derive(Clone, Copy, PartialEq, Debug)]
194#[repr(C)]
195/// A single BCM row payload for 16-bit plain output.
196///
197/// Row addressing, latch, OE, and pixel colour data are all encoded into the
198/// 16-bit `Entry` words -- no separate address bytes are needed.
199pub struct Row<const COLS: usize> {
200    pub(crate) data: [Entry; COLS],
201}
202
203impl<const COLS: usize> Row<COLS> {
204    /// Creates a zero-initialized row.
205    ///
206    /// Call [`Self::format`] before first use to populate row control metadata.
207    #[must_use]
208    pub const fn new() -> Self {
209        Self {
210            data: [Entry::new(); COLS],
211        }
212    }
213
214    /// Formats this row for the provided multiplexed row address.
215    ///
216    /// Sets up blanking delay, output-enable, latch, and address bits in the
217    /// pixel stream template.
218    #[inline]
219    pub fn format(&mut self, addr: u8, prev_addr: u8) {
220        let template = make_data_template::<COLS>(addr, prev_addr);
221        self.data.copy_from_slice(&template);
222    }
223}
224
225impl<const COLS: usize> Default for Row<COLS> {
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231/// A single bit-plane's DMA payload: all rows for the plane, optionally
232/// followed by a tail word (see the `tail-closes-latch` feature).
233#[derive(Clone, Copy, PartialEq, Debug)]
234#[repr(C)]
235pub struct PlaneData<const NROWS: usize, const COLS: usize> {
236    pub(crate) rows: [Row<COLS>; NROWS],
237    // Extra word that parks LATCH=0 and OE=BLANK on the GPIO pins after the
238    // last clock edge, preventing the latch from staying asserted between
239    // DMA descriptor loops.  On an esp32 the words are also re-ordered (swapped) so
240    // we need a padding word.
241    #[cfg(all(feature = "tail-closes-latch", feature = "esp32-ordering"))]
242    pub(crate) padding: Entry,
243    #[cfg(feature = "tail-closes-latch")]
244    pub(crate) tail: Entry,
245}
246
247impl<const NROWS: usize, const COLS: usize> PlaneData<NROWS, COLS> {
248    const fn new() -> Self {
249        Self {
250            rows: [Row::new(); NROWS],
251            #[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
252            padding: Entry::new(),
253            #[cfg(feature = "tail-closes-latch")]
254            tail: Entry::new(),
255        }
256    }
257}
258
259/// The entire BCM Frame Buffer (per-plane storage).
260#[derive(Copy, Clone)]
261#[repr(C)]
262pub struct DmaFrameBuffer<const NROWS: usize, const COLS: usize, const PLANES: usize> {
263    pub(crate) planes: [PlaneData<NROWS, COLS>; PLANES],
264}
265
266impl<const NROWS: usize, const COLS: usize, const PLANES: usize>
267    DmaFrameBuffer<NROWS, COLS, PLANES>
268{
269    /// Creates a new frame buffer, pre-formatted and ready for use.
270    #[must_use]
271    pub fn new() -> Self {
272        let mut instance = Self {
273            planes: [PlaneData::new(); PLANES],
274        };
275        instance.format();
276        instance
277    }
278
279    /// Returns the number of BCM chunks (one per bit-plane).
280    #[must_use]
281    pub const fn bcm_chunk_count() -> usize {
282        PLANES
283    }
284
285    /// Returns the byte size of one BCM chunk (a single bit-plane including tail word).
286    #[must_use]
287    pub const fn bcm_chunk_bytes() -> usize {
288        core::mem::size_of::<PlaneData<NROWS, COLS>>()
289    }
290
291    /// Formats the frame buffer with row addresses and control bits.
292    #[inline]
293    pub fn format(&mut self) {
294        for plane in &mut self.planes {
295            for (row_idx, row) in plane.rows.iter_mut().enumerate() {
296                let prev_addr = if row_idx == 0 {
297                    NROWS as u8 - 1
298                } else {
299                    row_idx as u8 - 1
300                };
301                row.format(row_idx as u8, prev_addr);
302            }
303            #[cfg(feature = "tail-closes-latch")]
304            {
305                plane.tail.0 = 0x1f | OE_BLANK;
306            }
307            #[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
308            {
309                plane.padding.0 = 0x1f | OE_BLANK;
310            }
311        }
312    }
313
314    /// Erase pixel colors while preserving row control data.
315    #[inline]
316    pub fn erase(&mut self) {
317        const MASK: u16 = !0b0111_1110_0000_0000; // clear bits 9-14 (R1,G1,B1,R2,G2,B2)
318        for plane in &mut self.planes {
319            for row in &mut plane.rows {
320                for entry in &mut row.data {
321                    entry.0 &= MASK;
322                }
323            }
324        }
325    }
326
327    /// Set a pixel in the framebuffer.
328    #[inline]
329    pub fn set_pixel(&mut self, p: Point, color: Color) {
330        if p.x < 0 || p.y < 0 {
331            return;
332        }
333        self.set_pixel_internal(p.x as usize, p.y as usize, color);
334    }
335
336    #[inline]
337    fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
338        if x >= COLS || y >= NROWS * 2 {
339            return;
340        }
341
342        let row_idx = if y < NROWS { y } else { y - NROWS };
343        let is_top = y < NROWS;
344        let red = color.r();
345        let green = color.g();
346        let blue = color.b();
347
348        for plane_idx in 0..PLANES {
349            let bit = 7_u32.saturating_sub(plane_idx as u32);
350            let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
351                | ((u8::from(((green >> bit) & 1) != 0)) << 1)
352                | u8::from(((red >> bit) & 1) != 0);
353            let col_idx = map_index(x);
354            let entry = &mut self.planes[plane_idx].rows[row_idx].data[col_idx];
355            if is_top {
356                entry.set_color0_bits(bits);
357            } else {
358                entry.set_color1_bits(bits);
359            }
360        }
361    }
362}
363
364impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
365    for DmaFrameBuffer<NROWS, COLS, PLANES>
366{
367    fn default() -> Self {
368        Self::new()
369    }
370}
371
372impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
373    for DmaFrameBuffer<NROWS, COLS, PLANES>
374{
375    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
376        f.debug_struct("DmaFrameBuffer")
377            .field("size", &core::mem::size_of_val(&self.planes))
378            .field("plane_count", &self.planes.len())
379            .field("plane_size", &core::mem::size_of_val(&self.planes[0]))
380            .finish()
381    }
382}
383
384#[cfg(feature = "defmt")]
385impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
386    for DmaFrameBuffer<NROWS, COLS, PLANES>
387{
388    fn format(&self, f: defmt::Formatter) {
389        defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
390        defmt::write!(f, " size: {}", core::mem::size_of_val(&self.planes));
391        defmt::write!(
392            f,
393            " plane_size: {}",
394            core::mem::size_of_val(&self.planes[0])
395        );
396    }
397}
398
399impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
400    for DmaFrameBuffer<NROWS, COLS, PLANES>
401{
402    type Word = u16;
403
404    fn plane_count(&self) -> usize {
405        PLANES
406    }
407
408    fn plane_ptr_len(&self, plane_idx: usize) -> (*const u8, usize) {
409        assert!(
410            plane_idx < PLANES,
411            "plane_idx {plane_idx} out of range for {PLANES} planes"
412        );
413        let ptr = (&raw const self.planes[plane_idx]).cast::<u8>();
414        let len = core::mem::size_of::<PlaneData<NROWS, COLS>>();
415        (ptr, len)
416    }
417}
418
419impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
420    for DmaFrameBuffer<NROWS, COLS, PLANES>
421{
422    #[inline]
423    fn erase(&mut self) {
424        DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
425    }
426
427    #[inline]
428    fn set_pixel(&mut self, p: Point, color: Color) {
429        DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
430    }
431}
432
433impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
434    for DmaFrameBuffer<NROWS, COLS, PLANES>
435{
436}
437
438impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
439    for DmaFrameBuffer<NROWS, COLS, PLANES>
440{
441    fn size(&self) -> Size {
442        Size::new(COLS as u32, (NROWS * 2) as u32)
443    }
444}
445
446impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
447    for DmaFrameBuffer<NROWS, COLS, PLANES>
448{
449    type Color = Color;
450    type Error = Infallible;
451
452    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
453    where
454        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
455    {
456        for pixel in pixels {
457            self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
458        }
459        Ok(())
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    extern crate std;
466
467    use super::*;
468    use embedded_graphics::prelude::*;
469    use std::format;
470
471    const TEST_COLS: usize = 64;
472    type TestBuffer = DmaFrameBuffer<16, 64, 8>;
473
474    #[test]
475    fn row_format_sets_address_and_control_bits() {
476        let mut row = Row::<TEST_COLS>::new();
477        row.format(5, 4);
478
479        let last_idx = map_index(TEST_COLS - 1);
480        assert_eq!(row.data[last_idx].latch(), true);
481        assert_eq!(row.data[last_idx].addr(), 5);
482
483        let first_idx = map_index(0);
484        assert_eq!(row.data[first_idx].addr(), 4);
485        assert_eq!(row.data[first_idx].latch(), false);
486    }
487
488    #[test]
489    fn format_sets_expected_row_addresses_for_all_rows() {
490        let mut fb = TestBuffer::new();
491        fb.format();
492
493        for plane_idx in 0..8 {
494            for row_idx in 0..16 {
495                let last_col = map_index(63);
496                assert_eq!(
497                    fb.planes[plane_idx].rows[row_idx].data[last_col].addr(),
498                    row_idx as u16
499                );
500                assert_eq!(
501                    fb.planes[plane_idx].rows[row_idx].data[last_col].latch(),
502                    true
503                );
504            }
505        }
506    }
507
508    #[test]
509    fn set_pixel_maps_top_half_bits_per_plane() {
510        let mut fb = TestBuffer::new();
511        let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
512        fb.set_pixel(Point::new(2, 3), color);
513
514        for plane_idx in 0..8 {
515            let bit = 7 - plane_idx;
516            let entry = fb.planes[plane_idx].rows[3].data[map_index(2)];
517            assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
518            assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
519            assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
520        }
521    }
522
523    #[test]
524    fn set_pixel_maps_bottom_half_bits_per_plane() {
525        let mut fb = TestBuffer::new();
526        let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
527        fb.set_pixel(Point::new(4, 20), color);
528
529        for plane_idx in 0..8 {
530            let bit = 7 - plane_idx;
531            let entry = fb.planes[plane_idx].rows[4].data[map_index(4)];
532            assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
533            assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
534            assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
535        }
536    }
537
538    #[test]
539    fn erase_clears_only_color_bits() {
540        let mut fb = TestBuffer::new();
541        let oe_before = fb.planes[0].rows[0].data[map_index(1)].output_enable();
542        fb.set_pixel(Point::new(0, 0), Color::WHITE);
543        fb.erase();
544
545        for plane in &fb.planes {
546            for row in &plane.rows {
547                for entry in &row.data {
548                    assert!(!entry.red1());
549                    assert!(!entry.grn1());
550                    assert!(!entry.blu1());
551                    assert!(!entry.red2());
552                    assert!(!entry.grn2());
553                    assert!(!entry.blu2());
554                }
555            }
556        }
557
558        assert_eq!(
559            fb.planes[0].rows[0].data[map_index(1)].output_enable(),
560            oe_before
561        );
562    }
563
564    #[test]
565    fn draw_target_iter_sets_pixels() {
566        let mut fb = TestBuffer::new();
567        let pixels = [Pixel(Point::new(1, 1), Color::RED)];
568        let result = fb.draw_iter(pixels);
569        assert!(result.is_ok());
570
571        for plane_idx in 0..8 {
572            let bit = 7 - plane_idx;
573            let entry = fb.planes[plane_idx].rows[1].data[map_index(1)];
574            assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
575            assert!(!entry.grn1());
576            assert!(!entry.blu1());
577        }
578    }
579
580    #[test]
581    fn set_pixel_ignores_out_of_bounds_and_negative() {
582        let mut fb = TestBuffer::new();
583        let before = fb.planes;
584        fb.set_pixel(Point::new(-1, 0), Color::WHITE);
585        fb.set_pixel(Point::new(0, -1), Color::WHITE);
586        fb.set_pixel(Point::new(64, 0), Color::WHITE);
587        fb.set_pixel(Point::new(0, 32), Color::WHITE);
588        assert_eq!(fb.planes, before);
589    }
590
591    #[test]
592    fn bcm_chunk_info_for_common_panel() {
593        assert_eq!(TestBuffer::bcm_chunk_count(), 8);
594        assert_eq!(
595            TestBuffer::bcm_chunk_bytes(),
596            core::mem::size_of::<PlaneData<16, 64>>()
597        );
598    }
599
600    #[test]
601    fn frame_buffer_trait_accessors_report_expected_values() {
602        let fb = TestBuffer::new();
603        let as_trait: &dyn FrameBuffer<Word = u16> = &fb;
604        assert_eq!(as_trait.get_word_size(), crate::WordSize::Sixteen);
605        assert_eq!(as_trait.plane_count(), 8);
606
607        let (ptr, len) = as_trait.plane_ptr_len(0);
608        assert_eq!(len, core::mem::size_of::<PlaneData<16, 64>>());
609        assert_eq!(ptr, (&raw const fb.planes[0]).cast::<u8>());
610    }
611
612    #[test]
613    #[should_panic(expected = "out of range")]
614    fn plane_ptr_len_panics_for_invalid_plane() {
615        let fb = TestBuffer::new();
616        let _ = fb.plane_ptr_len(8);
617    }
618
619    #[test]
620    fn origin_dimensions_match_panel_geometry() {
621        let fb = TestBuffer::new();
622        assert_eq!(fb.size(), Size::new(64, 32));
623    }
624
625    #[test]
626    fn debug_impl_includes_shape_information() {
627        let fb = TestBuffer::new();
628        let s = format!("{fb:?}");
629        assert!(s.contains("DmaFrameBuffer"));
630        assert!(s.contains("plane_count"));
631        assert!(s.contains("plane_size"));
632    }
633
634    #[test]
635    fn row_format_sets_expected_blank_and_latch_positions() {
636        let mut row = Row::<TEST_COLS>::new();
637        row.format(5, 4);
638
639        let oe_active = !cfg!(feature = "invert-oe");
640
641        // First active pixel at BLANKING_DELAY with previous address
642        let idx_active = map_index(BLANKING_DELAY);
643        assert_eq!(row.data[idx_active].output_enable(), oe_active);
644        assert_eq!(row.data[idx_active].addr(), 4);
645
646        // i == COLS - BLANKING_DELAY - 1 blanks output before latch
647        let idx_blank = map_index(TEST_COLS - BLANKING_DELAY - 1);
648        assert_eq!(row.data[idx_blank].output_enable(), !oe_active);
649
650        // i == COLS - 1 latches and switches to new address
651        let idx_last = map_index(TEST_COLS - 1);
652        assert!(row.data[idx_last].latch());
653        assert_eq!(row.data[idx_last].addr(), 5);
654    }
655
656    #[test]
657    fn default_constructors_match_new() {
658        let row_default = Row::<TEST_COLS>::default();
659        let row_new = Row::<TEST_COLS>::new();
660        assert_eq!(row_default, row_new);
661
662        let fb_default = TestBuffer::default();
663        let fb_new = TestBuffer::new();
664        assert_eq!(fb_default.planes, fb_new.planes);
665    }
666
667    #[test]
668    fn framebuffer_operations_trait_delegates_correctly() {
669        let mut fb = TestBuffer::new();
670        FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
671
672        assert!(fb.planes[0].rows[5].data[map_index(3)].grn1());
673
674        FrameBufferOperations::erase(&mut fb);
675        for plane in &fb.planes {
676            for row in &plane.rows {
677                for entry in &row.data {
678                    assert!(!entry.red1());
679                    assert!(!entry.grn1());
680                    assert!(!entry.blu1());
681                    assert!(!entry.red2());
682                    assert!(!entry.grn2());
683                    assert!(!entry.blu2());
684                }
685            }
686        }
687    }
688
689    #[test]
690    fn entry_debug_shows_hex_value() {
691        let mut entry = Entry::new();
692        entry.set_red1(true);
693        let s = format!("{entry:?}");
694        assert!(s.contains("Entry"));
695        assert!(s.contains("0x"));
696    }
697
698    #[test]
699    fn entry_new_oe_matches_feature() {
700        let entry = Entry::new();
701        if cfg!(feature = "invert-oe") {
702            assert!(entry.output_enable());
703        } else {
704            assert!(!entry.output_enable());
705        }
706    }
707
708    #[test]
709    fn make_data_template_oe_polarity() {
710        let mut row = Row::<TEST_COLS>::new();
711        row.format(5, 4);
712
713        let active_idx = map_index(BLANKING_DELAY);
714        let blank_idx = map_index(TEST_COLS - BLANKING_DELAY - 1);
715        let latch_idx = map_index(TEST_COLS - 1);
716
717        if cfg!(feature = "invert-oe") {
718            assert!(!row.data[active_idx].output_enable());
719            assert!(row.data[blank_idx].output_enable());
720            assert!(row.data[latch_idx].output_enable());
721        } else {
722            assert!(row.data[active_idx].output_enable());
723            assert!(!row.data[blank_idx].output_enable());
724            assert!(!row.data[latch_idx].output_enable());
725        }
726    }
727}