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