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