Skip to main content

hub75_framebuffer/
plain.rs

1//! DMA-friendly framebuffer implementation for HUB75 LED panels.
2//!
3//! This module provides a framebuffer implementation with memory
4//! layout optimized for efficient transfer to HUB75 LED panels. The data is
5//! structured for direct signal mapping, making it ideal for DMA transfers but
6//! also suitable for programmatic transfer. It supports RGB color and brightness
7//! control through multiple frames using Binary Code Modulation (BCM).
8//!
9//! # Hardware Requirements
10//! This implementation can be used by any microcontroller that has a peripheral
11//! capable of outputting a clock signal and 16 bits (though only 14 bits are
12//! actually needed) in parallel. The data is structured to directly match the
13//! HUB75 connector signals, making it suitable for various parallel output
14//! peripherals.
15//!
16//! # Features
17//! - Memory layout optimized for efficient data transfer
18//! - Direct signal mapping to HUB75 connector signals
19//! - Support for RGB color with brightness control
20//! - Multiple frame buffers for Binary Code Modulation (BCM)
21//! - Integration with embedded-graphics for easy drawing
22//!
23//! # Brightness Control
24//! Brightness is controlled through Binary Code Modulation (BCM):
25//! - The number of brightness levels is determined by the `BITS` parameter
26//! - Each additional bit doubles the number of brightness levels
27//! - More bits provide better brightness resolution but require more memory
28//! - Memory usage grows exponentially with the number of bits: `(2^BITS)-1`
29//!   frames
30//! - Example: 8 bits = 256 levels, 4 bits = 16 levels
31//!
32//! # Memory Usage
33//! The framebuffer's memory usage is determined by:
34//! - Panel size (ROWS × COLS)
35//! - Number of brightness bits (BITS)
36//! - Memory grows exponentially with bits: `(2^BITS)-1` frames
37//! - 16-bit entries provide direct signal mapping but use more memory
38//!
39//! # Example
40//! ```rust,no_run
41//! use embedded_graphics::pixelcolor::RgbColor;
42//! use embedded_graphics::prelude::*;
43//! use embedded_graphics::primitives::Circle;
44//! use embedded_graphics::primitives::Rectangle;
45//! use hub75_framebuffer::compute_frame_count;
46//! use hub75_framebuffer::compute_rows;
47//! use hub75_framebuffer::Color;
48//! use hub75_framebuffer::plain::DmaFrameBuffer;
49//! use embedded_graphics::primitives::PrimitiveStyle;
50//!
51//! // Create a framebuffer for a 64x32 panel with 3-bit color depth
52//! const ROWS: usize = 32;
53//! const COLS: usize = 64;
54//! const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
55//! const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
56//! const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
57//!
58//! let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
59//!
60//! // Draw a red rectangle
61//! Rectangle::new(Point::new(10, 10), Size::new(20, 20))
62//!     .into_styled(PrimitiveStyle::with_fill(Color::RED))
63//!     .draw(&mut framebuffer)
64//!     .unwrap();
65//!
66//! // Draw a blue circle
67//! Circle::new(Point::new(40, 20), 10)
68//!     .into_styled(PrimitiveStyle::with_fill(Color::BLUE))
69//!     .draw(&mut framebuffer)
70//!     .unwrap();
71//! ```
72//!
73//! # Implementation Details
74//! The framebuffer is organized to directly match the HUB75 connector signals:
75//! - Each 16-bit word maps directly to the HUB75 control signals
76//! - Color data (R, G, B) for two sub-pixels is stored in dedicated bits
77//! - Control signals (output enable, latch, address) are mapped to specific
78//!   bits
79//! - Multiple frames are used to achieve Binary Code Modulation (BCM)
80//! - DMA transfers the data directly to the panel without any transformation
81//!
82//! # HUB75 Signal Bit Mapping
83//! Each 16-bit `Entry` represents the logic levels that will be driven onto the HUB75
84//! bus during a single pixel-clock cycle. Every panel signal—apart from the clock
85//! (`CLK`) itself—occupies a dedicated bit, allowing the word to be streamed via DMA
86//! with zero run-time manipulation.
87//!
88//! ```text
89//! 15 ─ Dummy2   (spare)
90//! 14 ─ B2       Blue  – lower half of the panel
91//! 13 ─ G2       Green – lower half of the panel
92//! 12 ─ R2       Red   – lower half of the panel
93//! 11 ─ B1       Blue  – upper half of the panel
94//! 10 ─ G1       Green – upper half of the panel
95//!  9 ─ R1       Red   – upper half of the panel
96//!  8 ─ OE       Output-Enable / Blank
97//!  7 ─ Dummy1   (spare)
98//!  6 ─ Dummy0   (spare)
99//!  5 ─ LAT      Latch / STB
100//! 4-0 ─ A..E    Row address lines
101//! ```
102//!
103//! The pixel clock is generated by the peripheral that owns the DMA stream and
104//! is therefore **not** part of the 16-bit word stored in the framebuffer.
105//!
106//! # Binary Code Modulation (BCM) Frames
107//! Brightness is achieved with Binary-Code-Modulation as outlined in
108//! <https://www.batsocks.co.uk/readme/art_bcm_1.htm>. For a colour depth of
109//! `BITS`, the driver allocates `FRAME_COUNT = 2^BITS - 1` frames. Frame *n*
110//! (0-based) is displayed for a duration proportional to `2^n`, mirroring the
111//! weight of that bit in a binary number.
112//!
113//! When a pixel is written its 8-bit RGB components are compared against a
114//! per-frame threshold:
115//!
116//! ```text
117//! brightness_step = 256 / 2^BITS
118//! threshold_n     = (n + 1) * brightness_step
119//! ```
120//!
121//! If a channel's value is greater than or equal to `threshold_n` the
122//! corresponding colour bit is set in frame *n*. Streaming the frames from the
123//! least-significant (shortest) to the most-significant (longest) time-slot
124//! produces the correct 8-bit brightness while keeping the refresh routine
125//! trivial.
126//!
127//! # Safety
128//! This implementation uses unsafe code for DMA operations. The framebuffer
129//! must be properly aligned in memory and the DMA configuration must match the
130//! buffer layout.
131
132use core::convert::Infallible;
133
134use crate::{FrameBufferOperations, MutableFrameBuffer};
135use bitfield::bitfield;
136use embedded_dma::ReadBuffer;
137use embedded_graphics::pixelcolor::RgbColor;
138use embedded_graphics::prelude::Point;
139
140use super::Color;
141use super::FrameBuffer;
142
143#[cfg(feature = "blank-delay-1")]
144const BLANKING_DELAY: usize = 1;
145#[cfg(feature = "blank-delay-2")]
146const BLANKING_DELAY: usize = 2;
147#[cfg(feature = "blank-delay-4")]
148const BLANKING_DELAY: usize = 4;
149#[cfg(feature = "blank-delay-8")]
150const BLANKING_DELAY: usize = 8;
151
152// Default to 1 if no blanking delay feature is enabled
153#[cfg(not(any(
154    feature = "blank-delay-1",
155    feature = "blank-delay-2",
156    feature = "blank-delay-4",
157    feature = "blank-delay-8"
158)))]
159const BLANKING_DELAY: usize = 1;
160
161#[cfg(not(feature = "invert-oe"))]
162const OE_ACTIVE: u16 = 0b1_0000_0000;
163#[cfg(not(feature = "invert-oe"))]
164const OE_BLANK: u16 = 0;
165
166#[cfg(feature = "invert-oe")]
167const OE_ACTIVE: u16 = 0;
168#[cfg(feature = "invert-oe")]
169const OE_BLANK: u16 = 0b1_0000_0000;
170
171/// Creates a pre-computed data template for a row with the specified addresses.
172/// This template contains all the timing and control signals but no pixel data.
173#[inline]
174const fn make_data_template<const COLS: usize>(addr: u8, prev_addr: u8) -> [Entry; COLS] {
175    let mut data = [Entry::new(); COLS];
176    let mut i = 0;
177
178    while i < COLS {
179        let mut entry = Entry::new();
180        // start with blanking
181        entry.0 = prev_addr as u16 | OE_BLANK;
182
183        if i == COLS - 1 {
184            // last pixel is a latch and new address
185            entry.0 |= 0b0010_0000; // latch
186            entry.0 = (entry.0 & !0b0001_1111) | (addr as u16); // new address
187        } else if i >= BLANKING_DELAY && i < COLS - BLANKING_DELAY - 1 {
188            // active after blanking delay at the start and before blanking delay at the end
189            entry.0 = (entry.0 & !0b1_0000_0000) | OE_ACTIVE;
190        }
191
192        data[map_index(i)] = entry;
193        i += 1;
194    }
195
196    data
197}
198
199bitfield! {
200    /// A 16-bit word representing the HUB75 control signals for a single pixel.
201    ///
202    /// This structure directly maps to the HUB75 connector signals:
203    /// - RGB color data for two sub-pixels (color0 and color1)
204    /// - Panel control signals (output enable, latch, address)
205    /// - Dummy bits for timing alignment
206    ///
207    /// The bit layout matches the HUB75 connector signals:
208    /// - Bit 15: Dummy bit 2
209    /// - Bit 14: Blue channel for color1
210    /// - Bit 13: Green channel for color1
211    /// - Bit 12: Red channel for color1
212    /// - Bit 11: Blue channel for color0
213    /// - Bit 10: Green channel for color0
214    /// - Bit 9: Red channel for color0
215    /// - Bit 8: Output enable
216    /// - Bit 7: Dummy bit 1
217    /// - Bit 6: Dummy bit 0
218    /// - Bit 5: Latch signal
219    /// - Bits 4-0: Row address
220    #[derive(Clone, Copy, Default, PartialEq)]
221    #[repr(transparent)]
222    struct Entry(u16);
223    dummy2, set_dummy2: 15;
224    blu2, set_blu2: 14;
225    grn2, set_grn2: 13;
226    red2, set_red2: 12;
227    blu1, set_blu1: 11;
228    grn1, set_grn1: 10;
229    red1, set_red1: 9;
230    output_enable, set_output_enable: 8;
231    dummy1, set_dummy1: 7;
232    dummy0, set_dummy0: 6;
233    latch, set_latch: 5;
234    addr, set_addr: 4, 0;
235}
236
237impl core::fmt::Debug for Entry {
238    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
239        f.debug_tuple("Entry")
240            .field(&format_args!("{:#x}", self.0))
241            .finish()
242    }
243}
244
245#[cfg(feature = "defmt")]
246impl defmt::Format for Entry {
247    fn format(&self, f: defmt::Formatter) {
248        defmt::write!(f, "Entry({=u16:#x})", self.0);
249    }
250}
251
252impl Entry {
253    const fn new() -> Self {
254        #[cfg(feature = "invert-oe")]
255        {
256            Self(0b1_0000_0000)
257        }
258        #[cfg(not(feature = "invert-oe"))]
259        {
260            Self(0)
261        }
262    }
263
264    // Optimized color bit manipulation constants and methods
265    const COLOR0_MASK: u16 = 0b0000_1110_0000_0000; // bits 9-11: R1, G1, B1
266    const COLOR1_MASK: u16 = 0b0111_0000_0000_0000; // bits 12-14: R2, G2, B2
267
268    #[inline]
269    fn set_color0_bits(&mut self, bits: u8) {
270        let bits16 = u16::from(bits) << 9;
271        self.0 = (self.0 & !Self::COLOR0_MASK) | (bits16 & Self::COLOR0_MASK);
272    }
273
274    #[inline]
275    fn set_color1_bits(&mut self, bits: u8) {
276        let bits16 = u16::from(bits) << 12;
277        self.0 = (self.0 & !Self::COLOR1_MASK) | (bits16 & Self::COLOR1_MASK);
278    }
279}
280
281/// Represents a single row of pixels in the framebuffer.
282///
283/// Each row contains a fixed number of columns (`COLS`) and manages the timing
284/// and control signals for the HUB75 panel. The row handles:
285/// - Output enable timing to prevent ghosting
286/// - Latch signal generation for row updates
287/// - Row address management
288/// - Color data for both sub-pixels
289#[derive(Clone, Copy, PartialEq, Debug)]
290#[repr(C)]
291struct Row<const COLS: usize> {
292    data: [Entry; COLS],
293}
294
295const fn map_index(i: usize) -> usize {
296    #[cfg(feature = "esp32-ordering")]
297    {
298        i ^ 1
299    }
300    #[cfg(not(feature = "esp32-ordering"))]
301    {
302        i
303    }
304}
305
306impl<const COLS: usize> Default for Row<COLS> {
307    fn default() -> Self {
308        Self::new()
309    }
310}
311
312impl<const COLS: usize> Row<COLS> {
313    pub const fn new() -> Self {
314        Self {
315            data: [Entry::new(); COLS],
316        }
317    }
318
319    pub fn format(&mut self, addr: u8, prev_addr: u8) {
320        // Use pre-computed template and bulk copy for maximum performance
321        let template = make_data_template::<COLS>(addr, prev_addr);
322        self.data.copy_from_slice(&template);
323    }
324
325    /// Fast clear method that preserves timing/control bits while clearing pixel data.
326    /// Uses bulk memory operations for maximum performance.
327    #[inline]
328    pub fn clear_colors(&mut self) {
329        // Clear color bits while preserving timing and control bits
330        const COLOR_CLEAR_MASK: u16 = !0b0111_1110_0000_0000; // Clear bits 9-14 (R1,G1,B1,R2,G2,B2)
331
332        for entry in &mut self.data {
333            entry.0 &= COLOR_CLEAR_MASK;
334        }
335    }
336
337    #[inline]
338    pub fn set_color0(&mut self, col: usize, r: bool, g: bool, b: bool) {
339        let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
340        let col = map_index(col);
341        self.data[col].set_color0_bits(bits);
342    }
343
344    #[inline]
345    pub fn set_color1(&mut self, col: usize, r: bool, g: bool, b: bool) {
346        let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
347        let col = map_index(col);
348        self.data[col].set_color1_bits(bits);
349    }
350}
351
352#[derive(Copy, Clone, Debug)]
353#[repr(C)]
354struct Frame<const ROWS: usize, const COLS: usize, const NROWS: usize> {
355    rows: [Row<COLS>; NROWS],
356}
357
358impl<const ROWS: usize, const COLS: usize, const NROWS: usize> Frame<ROWS, COLS, NROWS> {
359    pub const fn new() -> Self {
360        Self {
361            rows: [Row::new(); NROWS],
362        }
363    }
364
365    pub fn format(&mut self) {
366        for (addr, row) in self.rows.iter_mut().enumerate() {
367            let prev_addr = if addr == 0 {
368                NROWS as u8 - 1
369            } else {
370                addr as u8 - 1
371            };
372            row.format(addr as u8, prev_addr);
373        }
374    }
375
376    /// Fast clear method that preserves timing/control bits while clearing pixel data.
377    #[inline]
378    pub fn clear_colors(&mut self) {
379        for row in &mut self.rows {
380            row.clear_colors();
381        }
382    }
383
384    #[inline]
385    pub fn set_pixel(&mut self, y: usize, x: usize, red: bool, green: bool, blue: bool) {
386        let row = &mut self.rows[if y < NROWS { y } else { y - NROWS }];
387        if y < NROWS {
388            row.set_color0(x, red, green, blue);
389        } else {
390            row.set_color1(x, red, green, blue);
391        }
392    }
393}
394
395impl<const ROWS: usize, const COLS: usize, const NROWS: usize> Default
396    for Frame<ROWS, COLS, NROWS>
397{
398    fn default() -> Self {
399        Self::new()
400    }
401}
402
403/// All BCM frames for this framebuffer, optionally followed by a tail word
404/// (see the `tail-closes-latch` feature).
405#[derive(Copy, Clone, Debug)]
406#[repr(C)]
407struct FrameData<const ROWS: usize, const COLS: usize, const NROWS: usize, const FRAME_COUNT: usize>
408{
409    frames: [Frame<ROWS, COLS, NROWS>; FRAME_COUNT],
410    #[cfg(all(feature = "tail-closes-latch", feature = "esp32-ordering"))]
411    padding: Entry,
412    #[cfg(feature = "tail-closes-latch")]
413    tail: Entry,
414}
415
416impl<const ROWS: usize, const COLS: usize, const NROWS: usize, const FRAME_COUNT: usize>
417    FrameData<ROWS, COLS, NROWS, FRAME_COUNT>
418{
419    const fn new() -> Self {
420        Self {
421            frames: [Frame::new(); FRAME_COUNT],
422            #[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
423            padding: Entry::new(),
424            #[cfg(feature = "tail-closes-latch")]
425            tail: Entry::new(),
426        }
427    }
428}
429
430/// DMA-compatible framebuffer for HUB75 LED panels.
431///
432/// This is a framebuffer implementation that:
433/// - Manages multiple frames for Binary Code Modulation (BCM)
434/// - Provides DMA-compatible memory layout
435/// - Implements the embedded-graphics `DrawTarget` trait
436///
437/// # Type Parameters
438/// - `ROWS`: Total number of rows in the panel
439/// - `COLS`: Number of columns in the panel
440/// - `NROWS`: Number of rows per scan (typically half of ROWS)
441/// - `BITS`: Color depth (1-8 bits)
442/// - `FRAME_COUNT`: Number of frames used for Binary Code Modulation
443///
444/// # Helper Functions
445/// Use these functions to compute the correct values:
446/// - `esp_hub75::compute_frame_count(BITS)`: Computes the required number of
447///   frames
448/// - `esp_hub75::compute_rows(ROWS)`: Computes the number of rows per scan
449///
450/// # Memory Layout
451/// The buffer is aligned to ensure efficient DMA transfers and contains:
452/// - A 64-bit alignment field
453/// - An array of frames, each containing the full panel data
454#[derive(Copy, Clone)]
455#[repr(C)]
456pub struct DmaFrameBuffer<
457    const ROWS: usize,
458    const COLS: usize,
459    const NROWS: usize,
460    const BITS: u8,
461    const FRAME_COUNT: usize,
462> {
463    _align: u64,
464    data: FrameData<ROWS, COLS, NROWS, FRAME_COUNT>,
465}
466
467impl<
468        const ROWS: usize,
469        const COLS: usize,
470        const NROWS: usize,
471        const BITS: u8,
472        const FRAME_COUNT: usize,
473    > Default for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
474{
475    fn default() -> Self {
476        Self::new()
477    }
478}
479
480impl<
481        const ROWS: usize,
482        const COLS: usize,
483        const NROWS: usize,
484        const BITS: u8,
485        const FRAME_COUNT: usize,
486    > DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
487{
488    /// Create a new, ready-to-use framebuffer.
489    ///
490    /// This creates a new framebuffer and automatically formats it with proper timing signals.
491    /// The framebuffer is immediately ready for pixel operations and DMA transfers.
492    ///
493    /// # Panics
494    ///
495    /// Panics if `BITS` is greater than 8, as only 1-8 bit color depths are supported.
496    ///
497    /// # Example
498    /// ```rust,no_run
499    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
500    ///
501    /// const ROWS: usize = 32;
502    /// const COLS: usize = 64;
503    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
504    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
505    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
506    ///
507    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
508    /// // No need to call format() - framebuffer is ready to use!
509    /// ```
510    #[must_use]
511    pub fn new() -> Self {
512        debug_assert!(BITS <= 8);
513
514        let mut instance = Self {
515            _align: 0,
516            data: FrameData::new(),
517        };
518
519        // Pre-format the framebuffer so it's immediately ready for use
520        instance.format();
521        instance
522    }
523
524    /// Returns the number of BCM chunks in this framebuffer (always 1 for
525    /// single-plane framebuffers — the entire buffer is one contiguous chunk).
526    #[must_use]
527    pub const fn bcm_chunk_count() -> usize {
528        1
529    }
530
531    /// Returns the byte size of one BCM chunk (for single-plane framebuffers
532    /// this equals the total DMA buffer size including the tail word, since BCM
533    /// weighting is baked in).
534    #[must_use]
535    pub const fn bcm_chunk_bytes() -> usize {
536        core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>()
537    }
538
539    /// Perform full formatting of the framebuffer with timing and control signals.
540    ///
541    /// This sets up all the timing and control signals needed for proper HUB75 operation.
542    /// This is automatically called by `new()`, so you typically don't need to call this
543    /// unless you want to completely reinitialize the framebuffer.
544    ///
545    /// # Example
546    /// ```rust,no_run
547    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
548    ///
549    /// const ROWS: usize = 32;
550    /// const COLS: usize = 64;
551    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
552    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
553    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
554    ///
555    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
556    /// framebuffer.format(); // Reinitialize if needed
557    /// ```
558    #[inline]
559    pub fn format(&mut self) {
560        for frame in &mut self.data.frames {
561            frame.format();
562        }
563        #[cfg(feature = "tail-closes-latch")]
564        {
565            self.data.tail.0 = 0x1f | OE_BLANK;
566        }
567        #[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
568        {
569            self.data.padding.0 = 0x1f | OE_BLANK;
570        }
571    }
572
573    /// Fast erase operation that clears all pixel data while preserving timing signals.
574    ///
575    /// This is much faster than `format()` when you just want to clear the display
576    /// since it preserves all the timing and control signals that are already set up.
577    /// Use this for clearing between frames or when you want to start drawing fresh content.
578    ///
579    /// # Example
580    /// ```rust,no_run
581    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
582    ///
583    /// const ROWS: usize = 32;
584    /// const COLS: usize = 64;
585    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
586    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
587    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
588    ///
589    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
590    /// framebuffer.erase();
591    /// ```
592    #[inline]
593    pub fn erase(&mut self) {
594        for frame in &mut self.data.frames {
595            frame.clear_colors();
596        }
597    }
598
599    /// Set a pixel in the framebuffer.
600    /// # Example
601    /// ```rust,no_run
602    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
603    /// use embedded_graphics::prelude::*;
604    ///
605    /// const ROWS: usize = 32;
606    /// const COLS: usize = 64;
607    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
608    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
609    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
610    ///
611    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
612    /// framebuffer.set_pixel(Point::new(10, 10), Color::RED);
613    /// ```
614    pub fn set_pixel(&mut self, p: Point, color: Color) {
615        if p.x < 0 || p.y < 0 {
616            return;
617        }
618        self.set_pixel_internal(p.x as usize, p.y as usize, color);
619    }
620
621    #[inline]
622    fn frames_on(v: u8) -> usize {
623        // v / brightness_step but the compiler resolves the shift at build-time
624        (v as usize) >> (8 - BITS)
625    }
626
627    #[inline]
628    fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
629        if x >= COLS || y >= ROWS {
630            return;
631        }
632
633        // Early exit for black pixels - common in UI backgrounds
634        // Only enabled when skip-black-pixels feature is active
635        #[cfg(feature = "skip-black-pixels")]
636        if color == Color::BLACK {
637            return;
638        }
639
640        // Pre-compute how many frames each channel should be on
641        let red_frames = Self::frames_on(color.r());
642        let green_frames = Self::frames_on(color.g());
643        let blue_frames = Self::frames_on(color.b());
644
645        // Set the pixel in all frames based on pre-computed frame counts
646        for (frame_idx, frame) in self.data.frames.iter_mut().enumerate() {
647            frame.set_pixel(
648                y,
649                x,
650                frame_idx < red_frames,
651                frame_idx < green_frames,
652                frame_idx < blue_frames,
653            );
654        }
655    }
656}
657
658impl<
659        const ROWS: usize,
660        const COLS: usize,
661        const NROWS: usize,
662        const BITS: u8,
663        const FRAME_COUNT: usize,
664    > FrameBufferOperations for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
665{
666    #[inline]
667    fn erase(&mut self) {
668        DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::erase(self);
669    }
670
671    #[inline]
672    fn set_pixel(&mut self, p: Point, color: Color) {
673        DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::set_pixel(self, p, color);
674    }
675}
676
677impl<
678        const ROWS: usize,
679        const COLS: usize,
680        const NROWS: usize,
681        const BITS: u8,
682        const FRAME_COUNT: usize,
683    > embedded_graphics::prelude::OriginDimensions
684    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
685{
686    fn size(&self) -> embedded_graphics::prelude::Size {
687        embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
688    }
689}
690
691impl<
692        const ROWS: usize,
693        const COLS: usize,
694        const NROWS: usize,
695        const BITS: u8,
696        const FRAME_COUNT: usize,
697    > embedded_graphics::prelude::OriginDimensions
698    for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
699{
700    fn size(&self) -> embedded_graphics::prelude::Size {
701        embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
702    }
703}
704
705impl<
706        const ROWS: usize,
707        const COLS: usize,
708        const NROWS: usize,
709        const BITS: u8,
710        const FRAME_COUNT: usize,
711    > embedded_graphics::draw_target::DrawTarget
712    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
713{
714    type Color = Color;
715
716    type Error = Infallible;
717
718    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
719    where
720        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
721    {
722        for pixel in pixels {
723            self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
724        }
725        Ok(())
726    }
727}
728
729unsafe impl<
730        const ROWS: usize,
731        const COLS: usize,
732        const NROWS: usize,
733        const BITS: u8,
734        const FRAME_COUNT: usize,
735    > ReadBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
736{
737    type Word = u8;
738
739    unsafe fn read_buffer(&self) -> (*const u8, usize) {
740        let ptr = (&raw const self.data).cast::<u8>();
741        let len = core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>();
742        (ptr, len)
743    }
744}
745
746unsafe impl<
747        const ROWS: usize,
748        const COLS: usize,
749        const NROWS: usize,
750        const BITS: u8,
751        const FRAME_COUNT: usize,
752    > ReadBuffer for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
753{
754    type Word = u8;
755
756    unsafe fn read_buffer(&self) -> (*const u8, usize) {
757        let ptr = (&raw const self.data).cast::<u8>();
758        let len = core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>();
759        (ptr, len)
760    }
761}
762
763impl<
764        const ROWS: usize,
765        const COLS: usize,
766        const NROWS: usize,
767        const BITS: u8,
768        const FRAME_COUNT: usize,
769    > core::fmt::Debug for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
770{
771    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
772        let brightness_step = 1 << (8 - BITS);
773        let size = core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>();
774        f.debug_struct("DmaFrameBuffer")
775            .field("size", &size)
776            .field("frame_count", &self.data.frames.len())
777            .field("frame_size", &core::mem::size_of_val(&self.data.frames[0]))
778            .field("brightness_step", &&brightness_step)
779            .finish_non_exhaustive()
780    }
781}
782
783#[cfg(feature = "defmt")]
784impl<
785        const ROWS: usize,
786        const COLS: usize,
787        const NROWS: usize,
788        const BITS: u8,
789        const FRAME_COUNT: usize,
790    > defmt::Format for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
791{
792    fn format(&self, f: defmt::Formatter) {
793        let brightness_step = 1 << (8 - BITS);
794        defmt::write!(
795            f,
796            "DmaFrameBuffer<{}, {}, {}, {}, {}>",
797            ROWS,
798            COLS,
799            NROWS,
800            BITS,
801            FRAME_COUNT
802        );
803        defmt::write!(
804            f,
805            " size: {}",
806            core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>()
807        );
808        defmt::write!(
809            f,
810            " frame_size: {}",
811            core::mem::size_of_val(&self.data.frames[0])
812        );
813        defmt::write!(f, " brightness_step: {}", brightness_step);
814    }
815}
816
817impl<
818        const ROWS: usize,
819        const COLS: usize,
820        const NROWS: usize,
821        const BITS: u8,
822        const FRAME_COUNT: usize,
823    > FrameBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
824{
825    type Word = u16;
826
827    fn plane_count(&self) -> usize {
828        1
829    }
830
831    fn plane_ptr_len(&self, plane_idx: usize) -> (*const u8, usize) {
832        assert!(plane_idx == 0, "plain DmaFrameBuffer has only 1 plane");
833        let ptr = (&raw const self.data).cast::<u8>();
834        let len = core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>();
835        (ptr, len)
836    }
837}
838
839impl<
840        const ROWS: usize,
841        const COLS: usize,
842        const NROWS: usize,
843        const BITS: u8,
844        const FRAME_COUNT: usize,
845    > MutableFrameBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
846{
847}
848
849#[cfg(test)]
850mod tests {
851    extern crate std;
852
853    use std::format;
854    use std::vec;
855
856    use super::*;
857    use crate::{FrameBuffer, WordSize};
858    use embedded_graphics::pixelcolor::RgbColor;
859    use embedded_graphics::prelude::*;
860    use embedded_graphics::primitives::{Circle, PrimitiveStyle, Rectangle};
861
862    const TEST_ROWS: usize = 32;
863    const TEST_COLS: usize = 64;
864    const TEST_NROWS: usize = TEST_ROWS / 2;
865    const TEST_BITS: u8 = 3;
866    const TEST_FRAME_COUNT: usize = (1 << TEST_BITS) - 1; // 7 frames for 3-bit depth
867
868    type TestFrameBuffer =
869        DmaFrameBuffer<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_BITS, TEST_FRAME_COUNT>;
870
871    // Helper function to get mapped index for ESP32
872    fn get_mapped_index(index: usize) -> usize {
873        map_index(index)
874    }
875
876    #[test]
877    fn test_entry_construction() {
878        let entry = Entry::new();
879        let expect_oe = cfg!(feature = "invert-oe");
880        assert_eq!(entry.dummy2(), false);
881        assert_eq!(entry.blu2(), false);
882        assert_eq!(entry.grn2(), false);
883        assert_eq!(entry.red2(), false);
884        assert_eq!(entry.blu1(), false);
885        assert_eq!(entry.grn1(), false);
886        assert_eq!(entry.red1(), false);
887        assert_eq!(entry.output_enable(), expect_oe);
888        assert_eq!(entry.dummy1(), false);
889        assert_eq!(entry.dummy0(), false);
890        assert_eq!(entry.latch(), false);
891        assert_eq!(entry.addr(), 0);
892    }
893
894    #[test]
895    fn test_entry_setters() {
896        let mut entry = Entry::new();
897
898        entry.set_dummy2(true);
899        assert_eq!(entry.dummy2(), true);
900        assert_eq!(entry.0 & 0b1000000000000000, 0b1000000000000000);
901
902        entry.set_blu2(true);
903        assert_eq!(entry.blu2(), true);
904        assert_eq!(entry.0 & 0b0100000000000000, 0b0100000000000000);
905
906        entry.set_grn2(true);
907        assert_eq!(entry.grn2(), true);
908        assert_eq!(entry.0 & 0b0010000000000000, 0b0010000000000000);
909
910        entry.set_red2(true);
911        assert_eq!(entry.red2(), true);
912        assert_eq!(entry.0 & 0b0001000000000000, 0b0001000000000000);
913
914        entry.set_blu1(true);
915        assert_eq!(entry.blu1(), true);
916        assert_eq!(entry.0 & 0b0000100000000000, 0b0000100000000000);
917
918        entry.set_grn1(true);
919        assert_eq!(entry.grn1(), true);
920        assert_eq!(entry.0 & 0b0000010000000000, 0b0000010000000000);
921
922        entry.set_red1(true);
923        assert_eq!(entry.red1(), true);
924        assert_eq!(entry.0 & 0b0000001000000000, 0b0000001000000000);
925
926        entry.set_output_enable(true);
927        assert_eq!(entry.output_enable(), true);
928        assert_eq!(entry.0 & 0b0000000100000000, 0b0000000100000000);
929
930        entry.set_dummy1(true);
931        assert_eq!(entry.dummy1(), true);
932        assert_eq!(entry.0 & 0b0000000010000000, 0b0000000010000000);
933
934        entry.set_dummy0(true);
935        assert_eq!(entry.dummy0(), true);
936        assert_eq!(entry.0 & 0b0000000001000000, 0b0000000001000000);
937
938        entry.set_latch(true);
939        assert_eq!(entry.latch(), true);
940        assert_eq!(entry.0 & 0b0000000000100000, 0b0000000000100000);
941
942        entry.set_addr(0b11111);
943        assert_eq!(entry.addr(), 0b11111);
944        assert_eq!(entry.0 & 0b0000000000011111, 0b0000000000011111);
945    }
946
947    #[test]
948    fn test_entry_bit_isolation() {
949        let mut entry = Entry::new();
950        let expect_oe = cfg!(feature = "invert-oe");
951
952        // Test that setting one field doesn't affect others
953        entry.set_addr(0b11111);
954        entry.set_latch(true);
955        assert_eq!(entry.addr(), 0b11111);
956        assert_eq!(entry.latch(), true);
957        assert_eq!(entry.output_enable(), expect_oe);
958        assert_eq!(entry.red1(), false);
959
960        entry.set_red1(true);
961        entry.set_grn2(true);
962        assert_eq!(entry.addr(), 0b11111);
963        assert_eq!(entry.latch(), true);
964        assert_eq!(entry.red1(), true);
965        assert_eq!(entry.grn2(), true);
966        assert_eq!(entry.blu1(), false);
967        assert_eq!(entry.red2(), false);
968    }
969
970    #[test]
971    fn test_entry_set_color0() {
972        let mut entry = Entry::new();
973
974        let bits = (u8::from(true) << 2) | (u8::from(false) << 1) | u8::from(true); // b=1, g=0, r=1 = 0b101
975        entry.set_color0_bits(bits);
976        assert_eq!(entry.red1(), true);
977        assert_eq!(entry.grn1(), false);
978        assert_eq!(entry.blu1(), true);
979        // Check that only the expected bits are set
980        assert_eq!(entry.0 & 0b0000101000000000, 0b0000101000000000); // Red1 and Blue1 bits
981    }
982
983    #[test]
984    fn test_entry_set_color1() {
985        let mut entry = Entry::new();
986
987        let bits = (u8::from(true) << 2) | (u8::from(true) << 1) | u8::from(false); // b=1, g=1, r=0 = 0b110
988        entry.set_color1_bits(bits);
989        assert_eq!(entry.red2(), false);
990        assert_eq!(entry.grn2(), true);
991        assert_eq!(entry.blu2(), true);
992        // Check that only the expected bits are set
993        assert_eq!(entry.0 & 0b0110000000000000, 0b0110000000000000); // Green2 and Blue2 bits
994    }
995
996    #[test]
997    fn test_entry_debug_formatting() {
998        let entry = Entry(0x1234);
999        let debug_str = format!("{:?}", entry);
1000        assert_eq!(debug_str, "Entry(0x1234)");
1001
1002        let entry = Entry(0xabcd);
1003        let debug_str = format!("{:?}", entry);
1004        assert_eq!(debug_str, "Entry(0xabcd)");
1005    }
1006
1007    #[test]
1008    fn test_row_construction() {
1009        let row: Row<TEST_COLS> = Row::new();
1010        assert_eq!(row.data.len(), TEST_COLS);
1011
1012        let expected = Entry::new().0;
1013        for entry in &row.data {
1014            assert_eq!(entry.0, expected);
1015        }
1016    }
1017
1018    #[test]
1019    fn test_row_format() {
1020        let mut row: Row<TEST_COLS> = Row::new();
1021        let test_addr = 5;
1022        let prev_addr = 4;
1023
1024        row.format(test_addr, prev_addr);
1025
1026        let oe_active = !cfg!(feature = "invert-oe");
1027
1028        // Check data entries configuration
1029        for (physical_i, entry) in row.data.iter().enumerate() {
1030            let logical_i = get_mapped_index(physical_i);
1031
1032            match logical_i {
1033                i if i == TEST_COLS - 1 => {
1034                    assert_eq!(entry.latch(), true);
1035                    assert_eq!(entry.addr(), test_addr as u16);
1036                    assert_eq!(entry.output_enable(), !oe_active);
1037                }
1038                _ => {
1039                    assert_eq!(entry.addr(), prev_addr as u16);
1040                    assert_eq!(entry.latch(), false);
1041                    if logical_i >= BLANKING_DELAY && logical_i < TEST_COLS - BLANKING_DELAY - 1 {
1042                        assert_eq!(entry.output_enable(), oe_active);
1043                    } else {
1044                        assert_eq!(entry.output_enable(), !oe_active);
1045                    }
1046                }
1047            }
1048        }
1049    }
1050
1051    #[test]
1052    fn test_row_set_color0() {
1053        let mut row: Row<TEST_COLS> = Row::new();
1054
1055        row.set_color0(0, true, false, true);
1056
1057        let mapped_col_0 = get_mapped_index(0);
1058        assert_eq!(row.data[mapped_col_0].red1(), true);
1059        assert_eq!(row.data[mapped_col_0].grn1(), false);
1060        assert_eq!(row.data[mapped_col_0].blu1(), true);
1061
1062        // Test another column
1063        row.set_color0(1, false, true, false);
1064
1065        let mapped_col_1 = get_mapped_index(1);
1066        assert_eq!(row.data[mapped_col_1].red1(), false);
1067        assert_eq!(row.data[mapped_col_1].grn1(), true);
1068        assert_eq!(row.data[mapped_col_1].blu1(), false);
1069    }
1070
1071    #[test]
1072    fn test_row_set_color1() {
1073        let mut row: Row<TEST_COLS> = Row::new();
1074
1075        row.set_color1(0, true, true, false);
1076
1077        let mapped_col_0 = get_mapped_index(0);
1078        assert_eq!(row.data[mapped_col_0].red2(), true);
1079        assert_eq!(row.data[mapped_col_0].grn2(), true);
1080        assert_eq!(row.data[mapped_col_0].blu2(), false);
1081    }
1082
1083    #[test]
1084    fn test_row_default() {
1085        let row1: Row<TEST_COLS> = Row::new();
1086        let row2: Row<TEST_COLS> = Row::default();
1087
1088        // Both should be equivalent
1089        assert_eq!(row1, row2);
1090        assert_eq!(row1.data.len(), row2.data.len());
1091
1092        let expected = Entry::new().0;
1093        for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
1094            assert_eq!(entry1.0, entry2.0);
1095            assert_eq!(entry1.0, expected);
1096        }
1097    }
1098
1099    #[test]
1100    fn test_frame_construction() {
1101        let frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1102        assert_eq!(frame.rows.len(), TEST_NROWS);
1103    }
1104
1105    #[test]
1106    fn test_frame_format() {
1107        let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1108
1109        frame.format();
1110
1111        // Check that each row was formatted with correct address parameters
1112        for addr in 0..TEST_NROWS {
1113            let prev_addr = if addr == 0 { TEST_NROWS - 1 } else { addr - 1 };
1114
1115            // Check some key pixels in each row
1116            let row = &frame.rows[addr];
1117
1118            // Check last pixel has correct new address
1119            let last_pixel_idx = get_mapped_index(TEST_COLS - 1);
1120            assert_eq!(row.data[last_pixel_idx].addr(), addr as u16);
1121            assert_eq!(row.data[last_pixel_idx].latch(), true);
1122
1123            // Check non-last pixels have previous address
1124            let first_pixel_idx = get_mapped_index(0);
1125            assert_eq!(row.data[first_pixel_idx].addr(), prev_addr as u16);
1126            assert_eq!(row.data[first_pixel_idx].latch(), false);
1127        }
1128    }
1129
1130    #[test]
1131    fn test_frame_set_pixel() {
1132        let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1133
1134        // Test setting pixel in upper half (y < NROWS)
1135        frame.set_pixel(5, 10, true, false, true);
1136
1137        let mapped_col_10 = get_mapped_index(10);
1138        assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1139        assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), false);
1140        assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), true);
1141
1142        // Test setting pixel in lower half (y >= NROWS)
1143        frame.set_pixel(TEST_NROWS + 5, 15, false, true, false);
1144
1145        let mapped_col_15 = get_mapped_index(15);
1146        assert_eq!(frame.rows[5].data[mapped_col_15].red2(), false);
1147        assert_eq!(frame.rows[5].data[mapped_col_15].grn2(), true);
1148        assert_eq!(frame.rows[5].data[mapped_col_15].blu2(), false);
1149    }
1150
1151    #[test]
1152    fn test_frame_default() {
1153        let frame1: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1154        let frame2: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::default();
1155
1156        // Both should be equivalent
1157        assert_eq!(frame1.rows.len(), frame2.rows.len());
1158
1159        let expected = Entry::new().0;
1160        for (row1, row2) in frame1.rows.iter().zip(frame2.rows.iter()) {
1161            assert_eq!(row1, row2);
1162
1163            for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
1164                assert_eq!(entry1.0, entry2.0);
1165                assert_eq!(entry1.0, expected);
1166            }
1167        }
1168    }
1169
1170    #[test]
1171    fn test_dma_framebuffer_construction() {
1172        let fb = TestFrameBuffer::new();
1173        assert_eq!(fb.data.frames.len(), TEST_FRAME_COUNT);
1174        assert_eq!(fb._align, 0);
1175    }
1176
1177    #[test]
1178    fn test_bcm_chunk_info() {
1179        let expected_size =
1180            core::mem::size_of::<FrameData<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_FRAME_COUNT>>();
1181        assert_eq!(TestFrameBuffer::bcm_chunk_bytes(), expected_size);
1182        assert_eq!(TestFrameBuffer::bcm_chunk_count(), 1);
1183    }
1184
1185    #[test]
1186    fn test_dma_framebuffer_erase() {
1187        let fb = TestFrameBuffer::new();
1188
1189        // After erasing, all frames should be formatted
1190        for frame in &fb.data.frames {
1191            for addr in 0..TEST_NROWS {
1192                let prev_addr = if addr == 0 { TEST_NROWS - 1 } else { addr - 1 };
1193
1194                // Check some key pixels in each row
1195                let row = &frame.rows[addr];
1196
1197                // Check last pixel has correct new address
1198                let last_pixel_idx = get_mapped_index(TEST_COLS - 1);
1199                assert_eq!(row.data[last_pixel_idx].addr(), addr as u16);
1200                assert_eq!(row.data[last_pixel_idx].latch(), true);
1201
1202                // Check non-last pixels have previous address
1203                let first_pixel_idx = get_mapped_index(0);
1204                assert_eq!(row.data[first_pixel_idx].addr(), prev_addr as u16);
1205                assert_eq!(row.data[first_pixel_idx].latch(), false);
1206            }
1207        }
1208    }
1209
1210    #[test]
1211    fn test_dma_framebuffer_set_pixel_bounds() {
1212        let mut fb = TestFrameBuffer::new();
1213
1214        // Test negative coordinates
1215        fb.set_pixel(Point::new(-1, 5), Color::RED);
1216        fb.set_pixel(Point::new(5, -1), Color::RED);
1217
1218        // Test coordinates out of bounds (should not panic)
1219        fb.set_pixel(Point::new(TEST_COLS as i32, 5), Color::RED);
1220        fb.set_pixel(Point::new(5, TEST_ROWS as i32), Color::RED);
1221    }
1222
1223    #[test]
1224    fn test_dma_framebuffer_set_pixel_internal() {
1225        let mut fb = TestFrameBuffer::new();
1226
1227        let red_color = Color::RED;
1228        fb.set_pixel_internal(10, 5, red_color);
1229
1230        // With 3-bit depth, brightness steps are 32 (256/8)
1231        // Frames represent thresholds: 32, 64, 96, 128, 160, 192, 224
1232        // Red value 255 should activate all frames
1233        for frame in &fb.data.frames {
1234            // Check upper half pixel
1235            let mapped_col_10 = get_mapped_index(10);
1236            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1237            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), false);
1238            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), false);
1239        }
1240    }
1241
1242    #[test]
1243    fn test_dma_framebuffer_brightness_modulation() {
1244        let mut fb = TestFrameBuffer::new();
1245
1246        // Test with a medium brightness value
1247        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1248        let test_brightness = brightness_step * 3; // 96
1249        let color = Color::from(embedded_graphics::pixelcolor::Rgb888::new(
1250            test_brightness,
1251            0,
1252            0,
1253        ));
1254
1255        fb.set_pixel_internal(0, 0, color);
1256
1257        // Should activate frames 0, 1, 2 (thresholds 32, 64, 96)
1258        // but not frames 3, 4, 5, 6 (thresholds 128, 160, 192, 224)
1259        for (frame_idx, frame) in fb.data.frames.iter().enumerate() {
1260            let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
1261            let should_be_active = test_brightness >= frame_threshold;
1262
1263            let mapped_col_0 = get_mapped_index(0);
1264            assert_eq!(frame.rows[0].data[mapped_col_0].red1(), should_be_active);
1265        }
1266    }
1267
1268    #[test]
1269    fn test_origin_dimensions() {
1270        let fb = TestFrameBuffer::new();
1271        let size = fb.size();
1272        assert_eq!(size.width, TEST_COLS as u32);
1273        assert_eq!(size.height, TEST_ROWS as u32);
1274
1275        // Test reference
1276        let size = (&fb).size();
1277        assert_eq!(size.width, TEST_COLS as u32);
1278        assert_eq!(size.height, TEST_ROWS as u32);
1279
1280        // Test mutable reference
1281        let mut fb = TestFrameBuffer::new();
1282        let fb_ref = &mut fb;
1283        let size = fb_ref.size();
1284        assert_eq!(size.width, TEST_COLS as u32);
1285        assert_eq!(size.height, TEST_ROWS as u32);
1286    }
1287
1288    #[test]
1289    fn test_draw_target() {
1290        let mut fb = TestFrameBuffer::new();
1291
1292        let pixels = vec![
1293            embedded_graphics::Pixel(Point::new(0, 0), Color::RED),
1294            embedded_graphics::Pixel(Point::new(1, 1), Color::GREEN),
1295            embedded_graphics::Pixel(Point::new(2, 2), Color::BLUE),
1296        ];
1297
1298        let result = fb.draw_iter(pixels);
1299        assert!(result.is_ok());
1300
1301        // Test mutable reference
1302        let result = (&mut fb).draw_iter(vec![embedded_graphics::Pixel(
1303            Point::new(3, 3),
1304            Color::WHITE,
1305        )]);
1306        assert!(result.is_ok());
1307    }
1308
1309    #[test]
1310    fn test_draw_iter_pixel_verification() {
1311        let mut fb = TestFrameBuffer::new();
1312
1313        // Create test pixels with specific colors and positions
1314        let pixels = vec![
1315            // Upper half pixels (y < NROWS) - should set color0
1316            embedded_graphics::Pixel(Point::new(5, 2), Color::RED), // (5, 2) -> red
1317            embedded_graphics::Pixel(Point::new(10, 5), Color::GREEN), // (10, 5) -> green
1318            embedded_graphics::Pixel(Point::new(15, 8), Color::BLUE), // (15, 8) -> blue
1319            embedded_graphics::Pixel(Point::new(20, 10), Color::WHITE), // (20, 10) -> white
1320            // Lower half pixels (y >= NROWS) - should set color1
1321            embedded_graphics::Pixel(Point::new(25, (TEST_NROWS + 3) as i32), Color::RED), // (25, 19) -> red
1322            embedded_graphics::Pixel(Point::new(30, (TEST_NROWS + 7) as i32), Color::GREEN), // (30, 23) -> green
1323            embedded_graphics::Pixel(Point::new(35, (TEST_NROWS + 12) as i32), Color::BLUE), // (35, 28) -> blue
1324            // Edge case: black pixel (should not be visible in first frame)
1325            embedded_graphics::Pixel(Point::new(40, 1), Color::BLACK), // (40, 1) -> black
1326        ];
1327
1328        let result = fb.draw_iter(pixels);
1329        assert!(result.is_ok());
1330
1331        // Check the first frame only
1332        let first_frame = &fb.data.frames[0];
1333        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1334        let first_frame_threshold = brightness_step; // 32
1335
1336        // Test upper half pixels (color0)
1337        // Red pixel at (5, 2) - should be red in first frame
1338        let col_idx = get_mapped_index(5);
1339        assert_eq!(
1340            first_frame.rows[2].data[col_idx].red1(),
1341            Color::RED.r() >= first_frame_threshold
1342        );
1343        assert_eq!(
1344            first_frame.rows[2].data[col_idx].grn1(),
1345            Color::RED.g() >= first_frame_threshold
1346        );
1347        assert_eq!(
1348            first_frame.rows[2].data[col_idx].blu1(),
1349            Color::RED.b() >= first_frame_threshold
1350        );
1351
1352        // Green pixel at (10, 5) - should be green in first frame
1353        let col_idx = get_mapped_index(10);
1354        assert_eq!(
1355            first_frame.rows[5].data[col_idx].red1(),
1356            Color::GREEN.r() >= first_frame_threshold
1357        );
1358        assert_eq!(
1359            first_frame.rows[5].data[col_idx].grn1(),
1360            Color::GREEN.g() >= first_frame_threshold
1361        );
1362        assert_eq!(
1363            first_frame.rows[5].data[col_idx].blu1(),
1364            Color::GREEN.b() >= first_frame_threshold
1365        );
1366
1367        // Blue pixel at (15, 8) - should be blue in first frame
1368        let col_idx = get_mapped_index(15);
1369        assert_eq!(
1370            first_frame.rows[8].data[col_idx].red1(),
1371            Color::BLUE.r() >= first_frame_threshold
1372        );
1373        assert_eq!(
1374            first_frame.rows[8].data[col_idx].grn1(),
1375            Color::BLUE.g() >= first_frame_threshold
1376        );
1377        assert_eq!(
1378            first_frame.rows[8].data[col_idx].blu1(),
1379            Color::BLUE.b() >= first_frame_threshold
1380        );
1381
1382        // White pixel at (20, 10) - should be white in first frame
1383        let col_idx = get_mapped_index(20);
1384        assert_eq!(
1385            first_frame.rows[10].data[col_idx].red1(),
1386            Color::WHITE.r() >= first_frame_threshold
1387        );
1388        assert_eq!(
1389            first_frame.rows[10].data[col_idx].grn1(),
1390            Color::WHITE.g() >= first_frame_threshold
1391        );
1392        assert_eq!(
1393            first_frame.rows[10].data[col_idx].blu1(),
1394            Color::WHITE.b() >= first_frame_threshold
1395        );
1396
1397        // Test lower half pixels (color1)
1398        // Red pixel at (25, TEST_NROWS + 3) -> row 3, color1
1399        let col_idx = get_mapped_index(25);
1400        assert_eq!(
1401            first_frame.rows[3].data[col_idx].red2(),
1402            Color::RED.r() >= first_frame_threshold
1403        );
1404        assert_eq!(
1405            first_frame.rows[3].data[col_idx].grn2(),
1406            Color::RED.g() >= first_frame_threshold
1407        );
1408        assert_eq!(
1409            first_frame.rows[3].data[col_idx].blu2(),
1410            Color::RED.b() >= first_frame_threshold
1411        );
1412
1413        // Green pixel at (30, TEST_NROWS + 7) -> row 7, color1
1414        let col_idx = get_mapped_index(30);
1415        assert_eq!(
1416            first_frame.rows[7].data[col_idx].red2(),
1417            Color::GREEN.r() >= first_frame_threshold
1418        );
1419        assert_eq!(
1420            first_frame.rows[7].data[col_idx].grn2(),
1421            Color::GREEN.g() >= first_frame_threshold
1422        );
1423        assert_eq!(
1424            first_frame.rows[7].data[col_idx].blu2(),
1425            Color::GREEN.b() >= first_frame_threshold
1426        );
1427
1428        // Blue pixel at (35, TEST_NROWS + 12) -> row 12, color1
1429        let col_idx = get_mapped_index(35);
1430        assert_eq!(
1431            first_frame.rows[12].data[col_idx].red2(),
1432            Color::BLUE.r() >= first_frame_threshold
1433        );
1434        assert_eq!(
1435            first_frame.rows[12].data[col_idx].grn2(),
1436            Color::BLUE.g() >= first_frame_threshold
1437        );
1438        assert_eq!(
1439            first_frame.rows[12].data[col_idx].blu2(),
1440            Color::BLUE.b() >= first_frame_threshold
1441        );
1442
1443        // Test black pixel - should not be visible in any frame
1444        let col_idx = get_mapped_index(40);
1445        assert_eq!(first_frame.rows[1].data[col_idx].red1(), false);
1446        assert_eq!(first_frame.rows[1].data[col_idx].grn1(), false);
1447        assert_eq!(first_frame.rows[1].data[col_idx].blu1(), false);
1448    }
1449
1450    #[test]
1451    fn test_embedded_graphics_integration() {
1452        let mut fb = TestFrameBuffer::new();
1453
1454        // Draw a rectangle
1455        let result = Rectangle::new(Point::new(5, 5), Size::new(10, 8))
1456            .into_styled(PrimitiveStyle::with_fill(Color::RED))
1457            .draw(&mut fb);
1458        assert!(result.is_ok());
1459
1460        // Draw a circle
1461        let result = Circle::new(Point::new(30, 15), 8)
1462            .into_styled(PrimitiveStyle::with_fill(Color::BLUE))
1463            .draw(&mut fb);
1464        assert!(result.is_ok());
1465    }
1466
1467    #[test]
1468    #[cfg(feature = "skip-black-pixels")]
1469    fn test_skip_black_pixels_enabled() {
1470        let mut fb = TestFrameBuffer::new();
1471
1472        // Set a red pixel first
1473        fb.set_pixel_internal(10, 5, Color::RED);
1474
1475        // Verify it's red in the first frame
1476        let mapped_col_10 = get_mapped_index(10);
1477        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].red1(), true);
1478        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1479        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1480
1481        // Now set it to black - with skip-black-pixels enabled, this should be ignored
1482        fb.set_pixel_internal(10, 5, Color::BLACK);
1483
1484        // The pixel should still be red (black write was skipped)
1485        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].red1(), true);
1486        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1487        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1488    }
1489
1490    #[test]
1491    #[cfg(not(feature = "skip-black-pixels"))]
1492    fn test_skip_black_pixels_disabled() {
1493        let mut fb = TestFrameBuffer::new();
1494
1495        // Set a red pixel first
1496        fb.set_pixel_internal(10, 5, Color::RED);
1497
1498        // Verify it's red in the first frame
1499        let mapped_col_10 = get_mapped_index(10);
1500        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].red1(), true);
1501        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1502        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1503
1504        // Now set it to black - with skip-black-pixels disabled, this should overwrite
1505        fb.set_pixel_internal(10, 5, Color::BLACK);
1506
1507        // The pixel should now be black (all bits false)
1508        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].red1(), false);
1509        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1510        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1511    }
1512
1513    #[test]
1514    fn test_bcm_frame_overwrite() {
1515        let mut fb = TestFrameBuffer::new();
1516
1517        // First write a white pixel (255, 255, 255)
1518        fb.set_pixel_internal(10, 5, Color::WHITE);
1519
1520        let mapped_col_10 = get_mapped_index(10);
1521
1522        // Verify white pixel is lit in all frames (255 >= all thresholds)
1523        for frame in fb.data.frames.iter() {
1524            // White (255) should be active in all frames since it's >= all thresholds
1525            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1526            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), true);
1527            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), true);
1528        }
1529
1530        // Now overwrite with 50% white (128, 128, 128)
1531        let half_white = Color::from(embedded_graphics::pixelcolor::Rgb888::new(128, 128, 128));
1532        fb.set_pixel_internal(10, 5, half_white);
1533
1534        // Verify only the correct frames are lit for 50% white
1535        // With 3-bit depth: thresholds are 32, 64, 96, 128, 160, 192, 224
1536        // 128 should activate frames 0, 1, 2, 3 (thresholds 32, 64, 96, 128)
1537        // but not frames 4, 5, 6 (thresholds 160, 192, 224)
1538        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1539        for (frame_idx, frame) in fb.data.frames.iter().enumerate() {
1540            let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
1541            let should_be_active = 128 >= frame_threshold;
1542
1543            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), should_be_active);
1544            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), should_be_active);
1545            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), should_be_active);
1546        }
1547
1548        // Specifically verify the expected pattern for 3-bit depth
1549        // Frames 0-3 should be active (thresholds 32, 64, 96, 128)
1550        for frame_idx in 0..4 {
1551            assert_eq!(
1552                fb.data.frames[frame_idx].rows[5].data[mapped_col_10].red1(),
1553                true
1554            );
1555        }
1556        // Frames 4-6 should be inactive (thresholds 160, 192, 224)
1557        for frame_idx in 4..TEST_FRAME_COUNT {
1558            assert_eq!(
1559                fb.data.frames[frame_idx].rows[5].data[mapped_col_10].red1(),
1560                false
1561            );
1562        }
1563    }
1564
1565    #[test]
1566    fn test_read_buffer_implementation() {
1567        // Test owned implementation - explicitly move the framebuffer to ensure we're testing the owned impl
1568        let fb = TestFrameBuffer::new();
1569        let expected_size =
1570            core::mem::size_of::<FrameData<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_FRAME_COUNT>>();
1571
1572        // Test owned ReadBuffer implementation by calling ReadBuffer::read_buffer explicitly
1573        unsafe {
1574            let (ptr, len) = <TestFrameBuffer as ReadBuffer>::read_buffer(&fb);
1575            assert!(!ptr.is_null());
1576            assert_eq!(len, expected_size);
1577        }
1578
1579        // Test direct method call on owned value
1580        unsafe {
1581            let (ptr, len) = fb.read_buffer();
1582            assert!(!ptr.is_null());
1583            assert_eq!(len, expected_size);
1584        }
1585
1586        // Test reference implementation
1587        let fb = TestFrameBuffer::new();
1588        let fb_ref = &fb;
1589        unsafe {
1590            let (ptr, len) = fb_ref.read_buffer();
1591            assert!(!ptr.is_null());
1592            assert_eq!(
1593                len,
1594                core::mem::size_of::<FrameData<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_FRAME_COUNT>>(
1595                )
1596            );
1597        }
1598
1599        // Test mutable reference implementation
1600        let mut fb = TestFrameBuffer::new();
1601        let fb_ref = &mut fb;
1602        unsafe {
1603            let (ptr, len) = fb_ref.read_buffer();
1604            assert!(!ptr.is_null());
1605            assert_eq!(
1606                len,
1607                core::mem::size_of::<FrameData<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_FRAME_COUNT>>(
1608                )
1609            );
1610        }
1611    }
1612
1613    #[test]
1614    fn test_read_buffer_owned_implementation() {
1615        // This test specifically ensures the owned ReadBuffer implementation is tested
1616        // by consuming the framebuffer and testing the pointer validity
1617        fn test_owned_read_buffer(fb: TestFrameBuffer) -> (bool, usize) {
1618            unsafe {
1619                let (ptr, len) = fb.read_buffer();
1620                (!ptr.is_null(), len)
1621            }
1622        }
1623
1624        let fb = TestFrameBuffer::new();
1625        let expected_len =
1626            core::mem::size_of::<FrameData<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_FRAME_COUNT>>();
1627
1628        let (ptr_valid, actual_len) = test_owned_read_buffer(fb);
1629        assert!(ptr_valid);
1630        assert_eq!(actual_len, expected_len);
1631    }
1632
1633    #[test]
1634    fn test_framebuffer_trait() {
1635        let fb = TestFrameBuffer::new();
1636        assert_eq!(fb.get_word_size(), WordSize::Sixteen);
1637
1638        let fb_ref = &fb;
1639        assert_eq!(fb_ref.get_word_size(), WordSize::Sixteen);
1640
1641        let mut fb = TestFrameBuffer::new();
1642        let fb_ref = &mut fb;
1643        assert_eq!(fb_ref.get_word_size(), WordSize::Sixteen);
1644    }
1645
1646    #[test]
1647    fn test_debug_formatting() {
1648        let fb = TestFrameBuffer::new();
1649        let debug_string = format!("{:?}", fb);
1650        assert!(debug_string.contains("DmaFrameBuffer"));
1651        assert!(debug_string.contains("frame_count"));
1652        assert!(debug_string.contains("frame_size"));
1653        assert!(debug_string.contains("brightness_step"));
1654    }
1655
1656    #[test]
1657    fn test_default_implementation() {
1658        let fb1 = TestFrameBuffer::new();
1659        let fb2 = TestFrameBuffer::default();
1660
1661        // Both should be equivalent in size, but may differ in content since new() calls format()
1662        assert_eq!(fb1.data.frames.len(), fb2.data.frames.len());
1663        assert_eq!(fb1._align, fb2._align);
1664    }
1665
1666    #[test]
1667    fn test_memory_alignment() {
1668        let fb = TestFrameBuffer::new();
1669        let ptr = &fb as *const _ as usize;
1670
1671        // Should be aligned to 64-bit boundary due to the _align field
1672        assert_eq!(ptr % 8, 0);
1673    }
1674
1675    #[test]
1676    fn test_color_values() {
1677        let mut fb = TestFrameBuffer::new();
1678
1679        // Test different color values
1680        let colors = [
1681            (Color::RED, (255, 0, 0)),
1682            (Color::GREEN, (0, 255, 0)),
1683            (Color::BLUE, (0, 0, 255)),
1684            (Color::WHITE, (255, 255, 255)),
1685            (Color::BLACK, (0, 0, 0)),
1686        ];
1687
1688        for (i, (color, (r, g, b))) in colors.iter().enumerate() {
1689            fb.set_pixel(Point::new(i as i32, 0), *color);
1690            assert_eq!(color.r(), *r);
1691            assert_eq!(color.g(), *g);
1692            assert_eq!(color.b(), *b);
1693        }
1694    }
1695
1696    #[test]
1697    fn test_blanking_delay() {
1698        let mut row: Row<TEST_COLS> = Row::new();
1699        let test_addr = 5;
1700        let prev_addr = 4;
1701
1702        row.format(test_addr, prev_addr);
1703
1704        let oe_active = !cfg!(feature = "invert-oe");
1705
1706        let blanking_pixel_idx = get_mapped_index(TEST_COLS - BLANKING_DELAY - 1);
1707        assert_eq!(row.data[blanking_pixel_idx].output_enable(), !oe_active);
1708
1709        let before_blanking_idx = get_mapped_index(TEST_COLS - BLANKING_DELAY - 2);
1710        assert_eq!(row.data[before_blanking_idx].output_enable(), oe_active);
1711    }
1712
1713    #[test]
1714    fn test_esp32_mapping() {
1715        // Test the ESP32-specific index mapping
1716        #[cfg(feature = "esp32-ordering")]
1717        {
1718            assert_eq!(map_index(0), 1);
1719            assert_eq!(map_index(1), 0);
1720            assert_eq!(map_index(2), 3);
1721            assert_eq!(map_index(3), 2);
1722            assert_eq!(map_index(4), 5);
1723            assert_eq!(map_index(5), 4);
1724        }
1725        #[cfg(not(feature = "esp32-ordering"))]
1726        {
1727            assert_eq!(map_index(0), 0);
1728            assert_eq!(map_index(1), 1);
1729            assert_eq!(map_index(2), 2);
1730            assert_eq!(map_index(3), 3);
1731        }
1732    }
1733
1734    #[test]
1735    fn test_bits_assertion() {
1736        // Test that BITS <= 8 assertion is enforced at compile time
1737        // This test mainly documents the constraint
1738        assert!(TEST_BITS <= 8);
1739    }
1740
1741    #[test]
1742    fn test_fast_clear_method() {
1743        let mut fb = TestFrameBuffer::new();
1744
1745        // Set some pixels
1746        fb.set_pixel_internal(10, 5, Color::RED);
1747        fb.set_pixel_internal(20, 10, Color::GREEN);
1748
1749        // Verify pixels are set
1750        let mapped_col_10 = get_mapped_index(10);
1751        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].red1(), true);
1752
1753        // Clear using fast method
1754        fb.erase();
1755
1756        // Verify pixels are cleared but timing signals remain
1757        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].red1(), false);
1758        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1759        assert_eq!(fb.data.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1760
1761        // Verify timing signals are still present (check last pixel has latch)
1762        let last_col = get_mapped_index(TEST_COLS - 1);
1763        assert_eq!(fb.data.frames[0].rows[5].data[last_col].latch(), true);
1764    }
1765
1766    // Remove the old `test_draw_char_bottom_right` and replace with a helper + combined test.
1767
1768    // Constants for FONT_6X10 glyph size
1769    const CHAR_W: i32 = 6;
1770    const CHAR_H: i32 = 10;
1771
1772    /// Draws glyph 'A' at `origin` and validates framebuffer pixels against a reference.
1773    fn verify_glyph_at(fb: &mut TestFrameBuffer, origin: Point) {
1774        use embedded_graphics::mock_display::MockDisplay;
1775        use embedded_graphics::mono_font::ascii::FONT_6X10;
1776        use embedded_graphics::mono_font::MonoTextStyle;
1777        use embedded_graphics::text::{Baseline, Text};
1778
1779        // Draw the glyph
1780        let style = MonoTextStyle::new(&FONT_6X10, Color::WHITE);
1781        Text::with_baseline("A", origin, style, Baseline::Top)
1782            .draw(fb)
1783            .unwrap();
1784
1785        // Reference glyph at (0,0)
1786        let mut reference: MockDisplay<Color> = MockDisplay::new();
1787        Text::with_baseline("A", Point::zero(), style, Baseline::Top)
1788            .draw(&mut reference)
1789            .unwrap();
1790
1791        for dy in 0..CHAR_H {
1792            for dx in 0..CHAR_W {
1793                let expected_on = reference
1794                    .get_pixel(Point::new(dx, dy))
1795                    .unwrap_or(Color::BLACK)
1796                    != Color::BLACK;
1797
1798                let gx = (origin.x + dx) as usize;
1799                let gy = (origin.y + dy) as usize;
1800
1801                // we have computed the origin to be within the panel, so we don't need to check for bounds
1802                // if gx >= TEST_COLS || gy >= TEST_ROWS {
1803                //     continue;
1804                // }
1805
1806                // Fetch Entry from frame 0
1807                let frame0 = &fb.data.frames[0];
1808                let e = if gy < TEST_NROWS {
1809                    &frame0.rows[gy].data[get_mapped_index(gx)]
1810                } else {
1811                    &frame0.rows[gy - TEST_NROWS].data[get_mapped_index(gx)]
1812                };
1813
1814                let (r, g, b) = if gy >= TEST_NROWS {
1815                    (e.red2(), e.grn2(), e.blu2())
1816                } else {
1817                    (e.red1(), e.grn1(), e.blu1())
1818                };
1819
1820                if expected_on {
1821                    assert!(r && g && b,);
1822                } else {
1823                    assert!(!r && !g && !b);
1824                }
1825            }
1826        }
1827    }
1828
1829    #[test]
1830    fn test_draw_char_corners() {
1831        let upper_left = Point::new(0, 0);
1832        let lower_right = Point::new(TEST_COLS as i32 - CHAR_W, TEST_ROWS as i32 - CHAR_H);
1833
1834        let mut fb = TestFrameBuffer::new();
1835
1836        verify_glyph_at(&mut fb, upper_left);
1837        verify_glyph_at(&mut fb, lower_right);
1838    }
1839
1840    #[test]
1841    fn test_framebuffer_operations_trait_erase() {
1842        let mut fb = TestFrameBuffer::new();
1843
1844        // Set pixels so erase has work to do
1845        fb.set_pixel_internal(10, 5, Color::RED);
1846        fb.set_pixel_internal(20, 10, Color::GREEN);
1847
1848        // Explicitly call trait method to hit the FrameBufferOperations impl
1849        <TestFrameBuffer as FrameBufferOperations>::erase(&mut fb);
1850
1851        // Verify colors cleared on frame 0
1852        let mc10 = get_mapped_index(10);
1853        let mc20 = get_mapped_index(20);
1854        assert_eq!(fb.data.frames[0].rows[5].data[mc10].red1(), false);
1855        assert_eq!(fb.data.frames[0].rows[10].data[mc20].grn1(), false);
1856
1857        // Timing signals preserved: last pixel should have latch
1858        let last_col = get_mapped_index(TEST_COLS - 1);
1859        assert!(fb.data.frames[0].rows[0].data[last_col].latch());
1860    }
1861
1862    #[test]
1863    fn test_framebuffer_operations_trait_set_pixel() {
1864        let mut fb = TestFrameBuffer::new();
1865
1866        // Explicitly call trait method to hit the FrameBufferOperations impl
1867        <TestFrameBuffer as FrameBufferOperations>::set_pixel(
1868            &mut fb,
1869            Point::new(8, 3),
1870            Color::BLUE,
1871        );
1872
1873        let idx = get_mapped_index(8);
1874        assert_eq!(fb.data.frames[0].rows[3].data[idx].blu1(), true);
1875        assert_eq!(fb.data.frames[0].rows[3].data[idx].red1(), false);
1876        assert_eq!(fb.data.frames[0].rows[3].data[idx].grn1(), false);
1877    }
1878
1879    #[test]
1880    fn test_entry_new_oe_matches_feature() {
1881        let entry = Entry::new();
1882        if cfg!(feature = "invert-oe") {
1883            assert!(entry.output_enable());
1884        } else {
1885            assert!(!entry.output_enable());
1886        }
1887    }
1888
1889    #[test]
1890    fn test_make_data_template_oe_polarity() {
1891        let mut row = Row::<TEST_COLS>::new();
1892        row.format(5, 4);
1893
1894        let active_idx = get_mapped_index(BLANKING_DELAY);
1895        let blank_idx = get_mapped_index(TEST_COLS - BLANKING_DELAY - 1);
1896        let latch_idx = get_mapped_index(TEST_COLS - 1);
1897
1898        if cfg!(feature = "invert-oe") {
1899            assert!(!row.data[active_idx].output_enable());
1900            assert!(row.data[blank_idx].output_enable());
1901            assert!(row.data[latch_idx].output_enable());
1902        } else {
1903            assert!(row.data[active_idx].output_enable());
1904            assert!(!row.data[blank_idx].output_enable());
1905            assert!(!row.data[latch_idx].output_enable());
1906        }
1907    }
1908}