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