hub75_framebuffer/
latched.rs

1//! DMA-friendly framebuffer implementation for HUB75 LED panels with external
2//! latch circuit support.
3//!
4//! This module provides a framebuffer implementation with memory
5//! layout optimized for efficient transfer to HUB75 LED panels. The data is
6//! structured for direct signal mapping, making it ideal for DMA transfers but
7//! also suitable for programmatic transfer. It supports RGB color and brightness
8//! control through multiple frames using Binary Code Modulation (BCM).
9//!
10//! # Hardware Requirements
11//! This implementation can be used by any microcontroller that has a peripheral
12//! capable of outputting a clock signal and 8 bits in parallel. A latch circuit
13//! similar to the one shown below can be used to hold the row address. The clock
14//! is gated so it does not reach the HUB75 interface when the latch is open.
15//! Since there is typically 4 2 input nand gates on a chip the 4th is used to allow
16//! PWM to gate the output enable providing much finer grained overall brightness control.
17//!
18// Important: note the blank line of documentation on each side of the image lookup table.
19// The "image lookup table" can be placed anywhere, but we place it here together with the
20// warning if the `doc-images` feature is not enabled.
21#![cfg_attr(feature = "doc-images",
22cfg_attr(all(),
23doc = ::embed_doc_image::embed_image!("latch-circuit", "images/latch-circuit.png")))]
24#![cfg_attr(
25    not(feature = "doc-images"),
26    doc = "**Doc images not enabled**. Compile with feature `doc-images` and Rust version >= 1.54 \
27           to enable."
28)]
29//!
30//! ![Latch Circuit][latch-circuit]
31//!
32//! # Key Differences from Plain Implementation
33//! - Uses an external latch circuit to hold the row address and gate the pixel
34//!   clock, reducing memory usage
35//! - 8-bit entries instead of 16-bit, halving memory requirements
36//! - Separate address and data words for better control
37//! - Requires an external latch circuit; not compatible with plain HUB75 wiring
38//!
39//! # Features
40//! - Support for RGB color with brightness control
41//! - Multiple frame buffers for Binary Code Modulation (BCM)
42//! - Integration with embedded-graphics for easy drawing
43//! - Memory-efficient 8-bit format
44//!
45//! # Brightness Control
46//! Brightness is controlled through Binary Code Modulation (BCM):
47//! - The number of brightness levels is determined by the `BITS` parameter
48//! - Each additional bit doubles the number of brightness levels
49//! - More bits provide better brightness resolution but require more memory
50//! - Memory usage grows exponentially with the number of bits: `(2^BITS)-1`
51//!   frames
52//! - Example: 8 bits = 256 levels, 4 bits = 16 levels
53//!
54//! # Memory Usage
55//! The framebuffer's memory usage is determined by:
56//! - Panel size (ROWS × COLS)
57//! - Number of brightness bits (BITS)
58//! - Memory grows exponentially with bits: `(2^BITS)-1` frames
59//! - 8-bit entries reduce memory usage compared to 16-bit implementations
60//!
61//! # Example
62//! ```rust
63//! use embedded_graphics::pixelcolor::RgbColor;
64//! use embedded_graphics::prelude::*;
65//! use embedded_graphics::primitives::Circle;
66//! use embedded_graphics::primitives::Rectangle;
67//! use embedded_graphics::primitives::PrimitiveStyle;
68//! use hub75_framebuffer::compute_frame_count;
69//! use hub75_framebuffer::compute_rows;
70//! use hub75_framebuffer::Color;
71//! use hub75_framebuffer::latched::DmaFrameBuffer;
72//!
73//! // Create a framebuffer for a 64x32 panel with 3-bit color depth
74//! const ROWS: usize = 32;
75//! const COLS: usize = 64;
76//! const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
77//! const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
78//! const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
79//!
80//! let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
81//!
82//! // Draw a red rectangle
83//! Rectangle::new(Point::new(10, 10), Size::new(20, 20))
84//!     .into_styled(PrimitiveStyle::with_fill(Color::RED))
85//!     .draw(&mut framebuffer)
86//!     .unwrap();
87//!
88//! // Draw a blue circle
89//! Circle::new(Point::new(40, 20), 10)
90//!     .into_styled(PrimitiveStyle::with_fill(Color::BLUE))
91//!     .draw(&mut framebuffer)
92//!     .unwrap();
93//! ```
94//!
95//! # Implementation Details
96//! The framebuffer is organized to efficiently use memory while maintaining
97//! HUB75 compatibility:
98//! - Each row contains both data and address words
99//! - 8-bit entries store RGB data for two sub-pixels
100//! - Separate address words control row selection and timing
101//! - Multiple frames are used to achieve Binary Code Modulation (BCM)
102//! - DMA transfers the data directly to the panel without
103//!   transformation
104//!
105//! # HUB75 Signal Bit Mapping (8-bit words)
106//! Two distinct 8-bit words are streamed to the panel:
107//!
108//! 1. **Address / Timing (`Address`)** – row-select and latch control.
109//! 2. **Pixel Data (`Entry`)**       – RGB bits for two sub-pixels plus OE/LAT shadow bits.
110//!
111//! The bit layouts intentionally overlap so that *the very same GPIO lines*
112//! can transmit either word without any run-time bit twiddling:
113//!
114//! ```text
115//! Address word (row select & timing)
116//! ┌──7─┬──6──┬─5─┬─4─┬─3─┬─2─┬─1─┬─0─┐
117//! │ OE │ LAT │   │ E │ D │ C │ B │ A │
118//! └────┴─────┴───┴───┴───┴───┴───┴───┘
119//!        ^                ^
120//!        |                └── Row-address lines (LSB = A)
121//!        └── Latch pulse – when HIGH the current address is latched and
122//!            external glue logic gates the pixel clock (`CLK`).
123//! ````
124//! ```text
125//! Entry word (pixel data for two sub-pixels)
126//! ┌──7─┬──6──┬─5──┬─4──┬─3──┬─2──┬─1──┬─0──┐
127//! │ OE │ LAT │ B2 │ G2 │ R2 │ B1 │ G1 │ R1 │
128//! └────┴─────┴────┴────┴────┴────┴────┴────┘
129//! ```
130//!
131//! *Bits 7–6* (OE/LAT) mirror those in the `Address` word so the control lines
132//! remain valid throughout the entire DMA stream.
133//!
134//! # External Latch Timing Sequence
135//! 1. Pixel data for row *N* is clocked out while `OE` is LOW.
136//! 2. `OE` is raised **HIGH** – LEDs blank.
137//! 3. An **`Address` word** with the new row index is transmitted while
138//!    `LAT` is HIGH; the CPLD/logic also blocks `CLK` during this period.
139//! 4. `LAT` returns LOW and `OE` is driven LOW again.
140//!
141//! This keeps visual artefacts to a minimum while allowing the framebuffer to
142//! use just 8 data bits.
143//!
144//! # Binary Code Modulation (BCM) Frames
145//! Brightness is realised with Binary-Code-Modulation just like the *plain*
146//! implementation—see <https://www.batsocks.co.uk/readme/art_bcm_1.htm>.
147//! With a colour depth of `BITS` the driver allocates
148//! `FRAME_COUNT = 2^BITS − 1` frames. Frame *n* (0-based) is displayed for a
149//! time slice proportional to `2^n`.
150//!
151//! For each channel the driver compares the 8-bit colour value against a per-frame
152//! threshold:
153//!
154//! ```text
155//! brightness_step = 256 / 2^BITS
156//! threshold_n     = (n + 1) * brightness_step
157//! ```
158//!
159//! The channel bit is set in frame *n* iff `value >= threshold_n`. Streaming the
160//! frames from LSB to MSB therefore reproduces the intended 8-bit intensity
161//! without extra processing.
162//!
163//! # Memory Layout
164//! Each row consists of:
165//! - 4 address words (8 bits each) for row selection and timing
166//! - COLS data words (8 bits each) for pixel data
167//!
168//! # Safety
169//! This implementation uses unsafe code for DMA operations. The framebuffer
170//! must be properly aligned in memory and the DMA configuration must match the
171//! buffer layout.
172use core::convert::Infallible;
173
174use super::Color;
175use crate::FrameBufferOperations;
176use bitfield::bitfield;
177#[cfg(not(feature = "esp-hal-dma"))]
178use embedded_dma::ReadBuffer;
179use embedded_graphics::pixelcolor::Rgb888;
180use embedded_graphics::pixelcolor::RgbColor;
181use embedded_graphics::prelude::Point;
182#[cfg(feature = "esp-hal-dma")]
183use esp_hal::dma::ReadBuffer;
184
185bitfield! {
186    /// 8-bit word carrying the row-address and timing control signals that are
187    /// driven on a HUB75 connector.
188    ///
189    /// Relationship to [`Entry`]
190    /// -------------------------
191    /// The control bits—output-enable (`OE`) and latch (`LAT`)—occupy **exactly**
192    /// the same bit positions as in [`Entry`].
193    /// This deliberate overlap allows both structures to be streamed through the
194    /// same GPIO/DMA path without any run-time bit remapping.
195    ///
196    /// Field summary
197    /// -------------
198    /// - Row-address lines `A`–`E` (5 bits)
199    /// - Latch signal `LAT`        (1 bit)
200    /// - Output-enable `OE`        (1 bit)
201    ///
202    /// Bit layout
203    /// ----------
204    /// - Bit 7 `OE`  : Output enable
205    /// - Bit 6 `LAT` : Row-latch strobe
206    ///   When asserted:
207    ///   1. The address bits (`A`–`E`) are latched by the panel driver.
208    ///   2. External glue logic gates the pixel clock (`CLK`), preventing any
209    ///      new pixel data from being shifted into the display while the latch
210    ///      is open.
211    /// - Bits 4–0 `A`–`E` : Row address (LSB =`A`)
212    ///
213    /// Behaviour notes
214    /// ---------------
215    /// * The address bits take effect only while `LAT` is high; they may be
216    ///   changed safely at any other time.
217    /// * Because `CLK` is inhibited during the latch interval, the pixel data
218    ///   stream produced from [`Entry`] words is paused until the latch is
219    ///   released.
220    #[derive(Clone, Copy, Default, PartialEq, Eq)]
221    #[repr(transparent)]
222    struct Address(u8);
223    impl Debug;
224    pub output_enable, set_output_enable: 7;
225    pub latch, set_latch: 6;
226    pub addr, set_addr: 4, 0;
227}
228
229impl Address {
230    pub const fn new() -> Self {
231        Self(0)
232    }
233}
234
235bitfield! {
236    /// 8-bit word representing the pixel data and control signals.
237    ///
238    /// This structure contains the RGB data for two sub-pixels and control signals:
239    /// - RGB data for two sub-pixels (color0 and color1)
240    /// - Output enable signal
241    /// - Latch signal
242    ///
243    /// The bit layout is as follows:
244    /// - Bit 7: Output enable
245    /// - Bit 6: Latch signal
246    /// - Bit 5: Blue channel for color1
247    /// - Bit 4: Green channel for color1
248    /// - Bit 3: Red channel for color1
249    /// - Bit 2: Blue channel for color0
250    /// - Bit 1: Green channel for color0
251    /// - Bit 0: Red channel for color0
252    #[derive(Clone, Copy, Default, PartialEq)]
253    #[repr(transparent)]
254    struct Entry(u8);
255    impl Debug;
256    pub output_enable, set_output_enable: 7;
257    pub latch, set_latch: 6;
258    pub blu2, set_blu2: 5;
259    pub grn2, set_grn2: 4;
260    pub red2, set_red2: 3;
261    pub blu1, set_blu1: 2;
262    pub grn1, set_grn1: 1;
263    pub red1, set_red1: 0;
264}
265
266impl Entry {
267    pub const fn new() -> Self {
268        Self(0)
269    }
270
271    // Optimized color bit manipulation constants and methods
272    const COLOR0_MASK: u8 = 0b0000_0111; // bits 0-2: R1, G1, B1
273    const COLOR1_MASK: u8 = 0b0011_1000; // bits 3-5: R2, G2, B2
274
275    #[inline]
276    fn set_color0_bits(&mut self, bits: u8) {
277        self.0 = (self.0 & !Self::COLOR0_MASK) | (bits & Self::COLOR0_MASK);
278    }
279
280    #[inline]
281    fn set_color1_bits(&mut self, bits: u8) {
282        self.0 = (self.0 & !Self::COLOR1_MASK) | ((bits << 3) & Self::COLOR1_MASK);
283    }
284}
285
286/// Represents a single row of pixels with external latch circuit support.
287///
288/// Each row contains both pixel data and address information:
289/// - 4 address words for row selection and timing
290/// - COLS data words for pixel data
291///
292/// The address words are arranged to match the external latch circuit's
293/// timing requirements. When the `esp32` feature is enabled, a specific
294/// mapping (2, 3, 0, 1) is applied to correct for the strange byte ordering
295/// required for the ESP32's I2S peripheral.
296#[derive(Clone, Copy, PartialEq, Debug)]
297#[repr(C)]
298struct Row<const COLS: usize> {
299    data: [Entry; COLS],
300    address: [Address; 4],
301}
302
303// bytes are output in the order 2, 3, 0, 1
304#[inline]
305const fn map_index(index: usize) -> usize {
306    #[cfg(feature = "esp32-ordering")]
307    {
308        index ^ 2
309    }
310    #[cfg(not(feature = "esp32-ordering"))]
311    {
312        index
313    }
314}
315
316/// Pre-computed address table for all possible row addresses (0-31).
317/// Each entry contains the 4 address words needed for that row.
318const fn make_addr_table() -> [[Address; 4]; 32] {
319    let mut tbl = [[Address::new(); 4]; 32];
320    let mut addr = 0;
321    while addr < 32 {
322        let mut i = 0;
323        while i < 4 {
324            let latch = i != 3;
325            let mapped_i = map_index(i);
326            let latch_bit = if latch { 1u8 << 6 } else { 0u8 };
327            tbl[addr][mapped_i].0 = latch_bit | addr as u8;
328            i += 1;
329        }
330        addr += 1;
331    }
332    tbl
333}
334
335static ADDR_TABLE: [[Address; 4]; 32] = make_addr_table();
336
337/// Pre-computed data template for a row with the given number of columns.
338/// This template has the correct OE/LAT bits set for each column position.
339const fn make_data_template<const COLS: usize>() -> [Entry; COLS] {
340    let mut data = [Entry::new(); COLS];
341    let mut i = 0;
342    while i < COLS {
343        let mapped_i = map_index(i);
344        // Set latch to false and output_enable to true for all except last column
345        // Note: Check the logical index (i), not the mapped index (mapped_i)
346        data[mapped_i].0 = if i == COLS - 1 { 0 } else { 0b1000_0000 }; // OE bit
347        i += 1;
348    }
349    data
350}
351
352impl<const COLS: usize> Row<COLS> {
353    pub const fn new() -> Self {
354        Self {
355            address: [Address::new(); 4],
356            data: [Entry::new(); COLS],
357        }
358    }
359
360    #[inline]
361    pub fn format(&mut self, addr: u8) {
362        // Use pre-computed address table
363        self.address.copy_from_slice(&ADDR_TABLE[addr as usize]);
364
365        // Use pre-computed data template - create it each time since we can't use generics in static
366        let data_template = make_data_template::<COLS>();
367        self.data.copy_from_slice(&data_template);
368    }
369
370    /// Fast clear that only zeros the color bits, preserving OE/LAT control bits
371    #[inline]
372    pub fn clear_colors(&mut self) {
373        // Clear color bits while preserving timing and control bits
374        const COLOR_CLEAR_MASK: u8 = !0b0011_1111; // Clear bits 0-5 (R1,G1,B1,R2,G2,B2)
375
376        for entry in &mut self.data {
377            entry.0 &= COLOR_CLEAR_MASK;
378        }
379    }
380
381    #[inline]
382    pub fn set_color0(&mut self, col: usize, r: bool, g: bool, b: bool) {
383        let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
384        let col = map_index(col);
385        self.data[col].set_color0_bits(bits);
386    }
387
388    #[inline]
389    pub fn set_color1(&mut self, col: usize, r: bool, g: bool, b: bool) {
390        let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
391        let col = map_index(col);
392        self.data[col].set_color1_bits(bits);
393    }
394}
395
396impl<const COLS: usize> Default for Row<COLS> {
397    fn default() -> Self {
398        Self::new()
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    #[inline]
416    pub fn format(&mut self) {
417        for (addr, row) in self.rows.iter_mut().enumerate() {
418            row.format(addr as u8);
419        }
420    }
421
422    /// Fast clear that only zeros the color bits, preserving control bits
423    #[inline]
424    pub fn clear_colors(&mut self) {
425        for row in &mut self.rows {
426            row.clear_colors();
427        }
428    }
429
430    #[inline]
431    pub fn set_pixel(&mut self, y: usize, x: usize, red: bool, green: bool, blue: bool) {
432        let row = &mut self.rows[if y < NROWS { y } else { y - NROWS }];
433        if y < NROWS {
434            row.set_color0(x, red, green, blue);
435        } else {
436            row.set_color1(x, red, green, blue);
437        }
438    }
439}
440
441impl<const ROWS: usize, const COLS: usize, const NROWS: usize> Default
442    for Frame<ROWS, COLS, NROWS>
443{
444    fn default() -> Self {
445        Self::new()
446    }
447}
448
449/// DMA-compatible framebuffer for HUB75 LED panels with external latch circuit
450/// support.
451///
452/// This implementation is optimized for memory usage and external latch circuit
453/// support:
454/// - Uses 8-bit entries instead of 16-bit
455/// - Separates address and data words
456/// - Supports the external latch circuit for row selection
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/// - An array of frames, each containing the full panel data
475/// - Each frame contains NROWS rows
476/// - Each row contains both data and address words
477#[derive(Copy, Clone)]
478#[repr(C)]
479#[repr(align(4))]
480pub struct DmaFrameBuffer<
481    const ROWS: usize,
482    const COLS: usize,
483    const NROWS: usize,
484    const BITS: u8,
485    const FRAME_COUNT: usize,
486> {
487    frames: [Frame<ROWS, COLS, NROWS>; FRAME_COUNT],
488}
489
490impl<
491        const ROWS: usize,
492        const COLS: usize,
493        const NROWS: usize,
494        const BITS: u8,
495        const FRAME_COUNT: usize,
496    > Default for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
497{
498    fn default() -> Self {
499        Self::new()
500    }
501}
502
503impl<
504        const ROWS: usize,
505        const COLS: usize,
506        const NROWS: usize,
507        const BITS: u8,
508        const FRAME_COUNT: usize,
509    > DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
510{
511    /// Create a new framebuffer with the given number of frames.
512    /// The framebuffer is automatically formatted and ready to use.
513    /// # Example
514    /// ```rust,no_run
515    /// use hub75_framebuffer::{latched::DmaFrameBuffer,compute_rows,compute_frame_count};
516    ///
517    /// const ROWS: usize = 32;
518    /// const COLS: usize = 64;
519    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
520    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
521    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
522    ///
523    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
524    /// // Ready to use immediately
525    /// ```
526    #[must_use]
527    pub fn new() -> Self {
528        let mut fb = Self {
529            frames: [Frame::new(); FRAME_COUNT],
530        };
531        fb.format();
532        fb
533    }
534
535    /// This returns the size of the DMA buffer in bytes.  Its used to calculate
536    /// the number of DMA descriptors needed for `esp-hal`.
537    /// # Example
538    /// ```rust,no_run
539    /// use hub75_framebuffer::{latched::DmaFrameBuffer,compute_rows,compute_frame_count};
540    ///
541    /// const ROWS: usize = 32;
542    /// const COLS: usize = 64;
543    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
544    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
545    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
546    ///
547    /// type FBType = DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>;
548    /// let (_, tx_descriptors) = esp_hal::dma_descriptors!(0, FBType::dma_buffer_size_bytes());
549    /// ```
550    #[cfg(feature = "esp-hal-dma")]
551    pub const fn dma_buffer_size_bytes() -> usize {
552        core::mem::size_of::<[Frame<ROWS, COLS, NROWS>; FRAME_COUNT]>()
553    }
554
555    /// Format the framebuffer, setting up all control bits and clearing pixel data.
556    /// This method does a full format of all control bits and clears all pixel data.
557    /// Normally you don't need to call this as `new()` automatically formats the framebuffer.
558    /// # Example
559    /// ```rust,no_run
560    /// use hub75_framebuffer::{Color,latched::DmaFrameBuffer,compute_rows,compute_frame_count};
561    ///
562    /// const ROWS: usize = 32;
563    /// const COLS: usize = 64;
564    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
565    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
566    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
567    ///
568    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
569    /// // framebuffer.format(); // Not needed - new() already calls this
570    /// ```
571    pub fn format(&mut self) {
572        for frame in &mut self.frames {
573            frame.format();
574        }
575    }
576
577    /// Erase pixel colors while preserving control bits.
578    /// This is much faster than `format()` and is the typical way to clear the display.
579    /// # Example
580    /// ```rust,no_run
581    /// use hub75_framebuffer::{Color,latched::DmaFrameBuffer,compute_rows,compute_frame_count};
582    ///
583    /// const ROWS: usize = 32;
584    /// const COLS: usize = 64;
585    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
586    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
587    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
588    ///
589    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
590    /// // ... draw some pixels ...
591    /// framebuffer.erase();
592    /// ```
593    #[inline]
594    pub fn erase(&mut self) {
595        for frame in &mut self.frames {
596            frame.clear_colors();
597        }
598    }
599
600    /// Set a pixel in the framebuffer.
601    /// # Example
602    /// ```rust,no_run
603    /// use hub75_framebuffer::{Color,latched::DmaFrameBuffer,compute_rows,compute_frame_count};
604    /// use embedded_graphics::prelude::*;
605    ///
606    /// const ROWS: usize = 32;
607    /// const COLS: usize = 64;
608    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
609    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
610    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
611    ///
612    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
613    /// framebuffer.set_pixel(Point::new(10, 10), Color::RED);
614    /// ```
615    pub fn set_pixel(&mut self, p: Point, color: Color) {
616        if p.x < 0 || p.y < 0 {
617            return;
618        }
619        self.set_pixel_internal(p.x as usize, p.y as usize, color);
620    }
621
622    #[inline]
623    fn frames_on(v: u8) -> usize {
624        // v / brightness_step but the compiler resolves the shift at build-time
625        (v as usize) >> (8 - BITS)
626    }
627
628    #[inline]
629    fn set_pixel_internal(&mut self, x: usize, y: usize, color: Rgb888) {
630        if x >= COLS || y >= ROWS {
631            return;
632        }
633
634        // Early exit for black pixels - common in UI backgrounds
635        // Only enabled when skip-black-pixels feature is active
636        #[cfg(feature = "skip-black-pixels")]
637        if color == Rgb888::BLACK {
638            return;
639        }
640
641        // Pre-compute how many frames each channel should be on
642        let red_frames = Self::frames_on(color.r());
643        let green_frames = Self::frames_on(color.g());
644        let blue_frames = Self::frames_on(color.b());
645
646        // Set the pixel in all frames based on pre-computed frame counts
647        for (frame_idx, frame) in self.frames.iter_mut().enumerate() {
648            frame.set_pixel(
649                y,
650                x,
651                frame_idx < red_frames,
652                frame_idx < green_frames,
653                frame_idx < blue_frames,
654            );
655        }
656    }
657}
658
659impl<
660        const ROWS: usize,
661        const COLS: usize,
662        const NROWS: usize,
663        const BITS: u8,
664        const FRAME_COUNT: usize,
665    > FrameBufferOperations<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
666    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
667{
668    #[inline]
669    fn erase(&mut self) {
670        DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::erase(self);
671    }
672
673    #[inline]
674    fn set_pixel(&mut self, p: Point, color: Color) {
675        DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::set_pixel(self, p, color);
676    }
677}
678
679impl<
680        const ROWS: usize,
681        const COLS: usize,
682        const NROWS: usize,
683        const BITS: u8,
684        const FRAME_COUNT: usize,
685    > embedded_graphics::prelude::OriginDimensions
686    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
687{
688    fn size(&self) -> embedded_graphics::prelude::Size {
689        embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
690    }
691}
692
693impl<
694        const ROWS: usize,
695        const COLS: usize,
696        const NROWS: usize,
697        const BITS: u8,
698        const FRAME_COUNT: usize,
699    > embedded_graphics::draw_target::DrawTarget
700    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
701{
702    type Color = Color;
703
704    type Error = Infallible;
705
706    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
707    where
708        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
709    {
710        for pixel in pixels {
711            self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
712        }
713        Ok(())
714    }
715}
716
717unsafe impl<
718        const ROWS: usize,
719        const COLS: usize,
720        const NROWS: usize,
721        const BITS: u8,
722        const FRAME_COUNT: usize,
723    > ReadBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
724{
725    #[cfg(not(feature = "esp-hal-dma"))]
726    type Word = u8;
727
728    unsafe fn read_buffer(&self) -> (*const u8, usize) {
729        let ptr = (&raw const self.frames).cast::<u8>();
730        let len = core::mem::size_of_val(&self.frames);
731        (ptr, len)
732    }
733}
734
735unsafe impl<
736        const ROWS: usize,
737        const COLS: usize,
738        const NROWS: usize,
739        const BITS: u8,
740        const FRAME_COUNT: usize,
741    > ReadBuffer for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
742{
743    #[cfg(not(feature = "esp-hal-dma"))]
744    type Word = u8;
745
746    unsafe fn read_buffer(&self) -> (*const u8, usize) {
747        let ptr = (&raw const self.frames).cast::<u8>();
748        let len = core::mem::size_of_val(&self.frames);
749        (ptr, len)
750    }
751}
752
753impl<
754        const ROWS: usize,
755        const COLS: usize,
756        const NROWS: usize,
757        const BITS: u8,
758        const FRAME_COUNT: usize,
759    > core::fmt::Debug for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
760{
761    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
762        let brightness_step = 1 << (8 - BITS);
763        f.debug_struct("DmaFrameBuffer")
764            .field("size", &core::mem::size_of_val(&self.frames))
765            .field("frame_count", &self.frames.len())
766            .field("frame_size", &core::mem::size_of_val(&self.frames[0]))
767            .field("brightness_step", &&brightness_step)
768            .finish()
769    }
770}
771
772#[cfg(feature = "defmt")]
773impl<
774        const ROWS: usize,
775        const COLS: usize,
776        const NROWS: usize,
777        const BITS: u8,
778        const FRAME_COUNT: usize,
779    > defmt::Format for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
780{
781    fn format(&self, f: defmt::Formatter) {
782        let brightness_step = 1 << (8 - BITS);
783        defmt::write!(
784            f,
785            "DmaFrameBuffer<{}, {}, {}, {}, {}>",
786            ROWS,
787            COLS,
788            NROWS,
789            BITS,
790            FRAME_COUNT
791        );
792        defmt::write!(f, " size: {}", core::mem::size_of_val(&self.frames));
793        defmt::write!(
794            f,
795            " frame_size: {}",
796            core::mem::size_of_val(&self.frames[0])
797        );
798        defmt::write!(f, " brightness_step: {}", brightness_step);
799    }
800}
801
802impl<
803        const ROWS: usize,
804        const COLS: usize,
805        const NROWS: usize,
806        const BITS: u8,
807        const FRAME_COUNT: usize,
808    > super::FrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
809    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
810{
811    fn get_word_size(&self) -> super::WordSize {
812        super::WordSize::Eight
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    > embedded_graphics::prelude::OriginDimensions
823    for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
824{
825    fn size(&self) -> embedded_graphics::prelude::Size {
826        embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
827    }
828}
829
830impl<
831        const ROWS: usize,
832        const COLS: usize,
833        const NROWS: usize,
834        const BITS: u8,
835        const FRAME_COUNT: usize,
836    > super::FrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
837    for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
838{
839    fn get_word_size(&self) -> super::WordSize {
840        super::WordSize::Eight
841    }
842}
843
844#[cfg(test)]
845mod tests {
846    extern crate std;
847
848    use std::format;
849    use std::vec;
850
851    use super::*;
852    use crate::{FrameBuffer, WordSize};
853    use embedded_graphics::pixelcolor::RgbColor;
854    use embedded_graphics::prelude::*;
855    use embedded_graphics::primitives::{Circle, PrimitiveStyle, Rectangle};
856
857    const TEST_ROWS: usize = 32;
858    const TEST_COLS: usize = 64;
859    const TEST_NROWS: usize = TEST_ROWS / 2;
860    const TEST_BITS: u8 = 3;
861    const TEST_FRAME_COUNT: usize = (1 << TEST_BITS) - 1; // 7 frames for 3-bit depth
862
863    type TestFrameBuffer =
864        DmaFrameBuffer<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_BITS, TEST_FRAME_COUNT>;
865
866    #[test]
867    fn test_address_construction() {
868        let addr = Address::new();
869        assert_eq!(addr.0, 0);
870        assert_eq!(addr.latch(), false);
871        assert_eq!(addr.addr(), 0);
872    }
873
874    #[test]
875    fn test_address_setters() {
876        let mut addr = Address::new();
877
878        addr.set_latch(true);
879        assert_eq!(addr.latch(), true);
880        assert_eq!(addr.0 & 0b01000000, 0b01000000);
881
882        addr.set_addr(0b11111);
883        assert_eq!(addr.addr(), 0b11111);
884        assert_eq!(addr.0 & 0b00011111, 0b00011111);
885    }
886
887    #[test]
888    fn test_address_bit_isolation() {
889        let mut addr = Address::new();
890
891        // Test that setting one field doesn't affect others
892        addr.set_addr(0b11111);
893        addr.set_latch(true);
894        assert_eq!(addr.addr(), 0b11111);
895        assert_eq!(addr.latch(), true);
896    }
897
898    #[test]
899    fn test_entry_construction() {
900        let entry = Entry::new();
901        assert_eq!(entry.0, 0);
902        assert_eq!(entry.output_enable(), false);
903        assert_eq!(entry.latch(), false);
904        assert_eq!(entry.red1(), false);
905        assert_eq!(entry.grn1(), false);
906        assert_eq!(entry.blu1(), false);
907        assert_eq!(entry.red2(), false);
908        assert_eq!(entry.grn2(), false);
909        assert_eq!(entry.blu2(), false);
910    }
911
912    #[test]
913    fn test_entry_setters() {
914        let mut entry = Entry::new();
915
916        entry.set_output_enable(true);
917        assert_eq!(entry.output_enable(), true);
918        assert_eq!(entry.0 & 0b10000000, 0b10000000);
919
920        entry.set_latch(true);
921        assert_eq!(entry.latch(), true);
922        assert_eq!(entry.0 & 0b01000000, 0b01000000);
923
924        // Test RGB channels for color0 (bits 0-2)
925        entry.set_red1(true);
926        entry.set_grn1(true);
927        entry.set_blu1(true);
928        assert_eq!(entry.red1(), true);
929        assert_eq!(entry.grn1(), true);
930        assert_eq!(entry.blu1(), true);
931        assert_eq!(entry.0 & 0b00000111, 0b00000111);
932
933        // Test RGB channels for color1 (bits 3-5)
934        entry.set_red2(true);
935        entry.set_grn2(true);
936        entry.set_blu2(true);
937        assert_eq!(entry.red2(), true);
938        assert_eq!(entry.grn2(), true);
939        assert_eq!(entry.blu2(), true);
940        assert_eq!(entry.0 & 0b00111000, 0b00111000);
941    }
942
943    #[test]
944    fn test_entry_set_color0() {
945        let mut entry = Entry::new();
946
947        let bits = (u8::from(true) << 2) | (u8::from(false) << 1) | u8::from(true); // b=1, g=0, r=1 = 0b101
948        entry.set_color0_bits(bits);
949        assert_eq!(entry.red1(), true);
950        assert_eq!(entry.grn1(), false);
951        assert_eq!(entry.blu1(), true);
952        assert_eq!(entry.0 & 0b00000111, 0b00000101); // Red and blue bits set
953    }
954
955    #[test]
956    fn test_entry_set_color1() {
957        let mut entry = Entry::new();
958
959        let bits = (u8::from(true) << 2) | (u8::from(true) << 1) | u8::from(false); // b=1, g=1, r=0 = 0b110
960        entry.set_color1_bits(bits);
961        assert_eq!(entry.red2(), false);
962        assert_eq!(entry.grn2(), true);
963        assert_eq!(entry.blu2(), true);
964        assert_eq!(entry.0 & 0b00111000, 0b00110000); // Green and blue bits set
965    }
966
967    #[test]
968    fn test_row_construction() {
969        let row: Row<TEST_COLS> = Row::new();
970        assert_eq!(row.data.len(), TEST_COLS);
971        assert_eq!(row.address.len(), 4);
972
973        // Check that all entries are initialized to zero
974        for entry in &row.data {
975            assert_eq!(entry.0, 0);
976        }
977        for addr in &row.address {
978            assert_eq!(addr.0, 0);
979        }
980    }
981
982    #[test]
983    fn test_row_format() {
984        let mut row: Row<TEST_COLS> = Row::new();
985        let test_addr = 5;
986
987        row.format(test_addr);
988
989        // Check address words configuration
990        for addr in &row.address {
991            assert_eq!(addr.addr(), test_addr);
992            // The latch values are pre-computed in the address table based on the logical
993            // arrangement, so we don't need to reverse-map. Just verify the table matches
994            // what we expect from the make_addr_table function.
995        }
996        // Since the address table is complex with ESP32 mapping, let's just verify
997        // that exactly one address has latch=false (from logical index 3) and the
998        // rest have latch=true
999        let latch_false_count = row.address.iter().filter(|addr| !addr.latch()).count();
1000        assert_eq!(latch_false_count, 1);
1001
1002        // Check data entries configuration
1003        for entry in &row.data {
1004            assert_eq!(entry.latch(), false);
1005        }
1006        // The output enable bits are pre-computed in the data template with ESP32 mapping
1007        // taken into account. Since make_data_template checks the logical index (i) not
1008        // the mapped index, exactly one entry should have output_enable=false (the one
1009        // corresponding to the last logical column)
1010        let oe_false_count = row
1011            .data
1012            .iter()
1013            .filter(|entry| !entry.output_enable())
1014            .count();
1015        assert_eq!(oe_false_count, 1);
1016    }
1017
1018    #[test]
1019    fn test_row_set_color0() {
1020        let mut row: Row<TEST_COLS> = Row::new();
1021
1022        row.set_color0(0, true, false, true);
1023
1024        let mapped_col_0 = map_index(0);
1025        assert_eq!(row.data[mapped_col_0].red1(), true);
1026        assert_eq!(row.data[mapped_col_0].grn1(), false);
1027        assert_eq!(row.data[mapped_col_0].blu1(), true);
1028
1029        // Test another column
1030        row.set_color0(1, false, true, false);
1031
1032        let mapped_col_1 = map_index(1);
1033        assert_eq!(row.data[mapped_col_1].red1(), false);
1034        assert_eq!(row.data[mapped_col_1].grn1(), true);
1035        assert_eq!(row.data[mapped_col_1].blu1(), false);
1036    }
1037
1038    #[test]
1039    fn test_row_set_color1() {
1040        let mut row: Row<TEST_COLS> = Row::new();
1041
1042        row.set_color1(0, true, true, false);
1043
1044        let mapped_col_0 = map_index(0);
1045        assert_eq!(row.data[mapped_col_0].red2(), true);
1046        assert_eq!(row.data[mapped_col_0].grn2(), true);
1047        assert_eq!(row.data[mapped_col_0].blu2(), false);
1048    }
1049
1050    #[test]
1051    fn test_frame_construction() {
1052        let frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1053        assert_eq!(frame.rows.len(), TEST_NROWS);
1054    }
1055
1056    #[test]
1057    fn test_frame_format() {
1058        let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1059
1060        frame.format();
1061
1062        for (addr, row) in frame.rows.iter().enumerate() {
1063            // Check that each row was formatted with its address
1064            for address in &row.address {
1065                assert_eq!(address.addr() as usize, addr);
1066            }
1067        }
1068    }
1069
1070    #[test]
1071    fn test_frame_set_pixel() {
1072        let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1073
1074        // Test setting pixel in upper half (y < NROWS)
1075        frame.set_pixel(5, 10, true, false, true);
1076
1077        let mapped_col_10 = map_index(10);
1078        assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1079        assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), false);
1080        assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), true);
1081
1082        // Test setting pixel in lower half (y >= NROWS)
1083        frame.set_pixel(TEST_NROWS + 5, 15, false, true, false);
1084
1085        let mapped_col_15 = map_index(15);
1086        assert_eq!(frame.rows[5].data[mapped_col_15].red2(), false);
1087        assert_eq!(frame.rows[5].data[mapped_col_15].grn2(), true);
1088        assert_eq!(frame.rows[5].data[mapped_col_15].blu2(), false);
1089    }
1090
1091    #[test]
1092    fn test_row_default() {
1093        let row1: Row<TEST_COLS> = Row::new();
1094        let row2: Row<TEST_COLS> = Row::default();
1095
1096        // Both should be equivalent
1097        assert_eq!(row1, row2);
1098        assert_eq!(row1.data.len(), row2.data.len());
1099        assert_eq!(row1.address.len(), row2.address.len());
1100
1101        // Check that all entries are initialized to zero
1102        for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
1103            assert_eq!(entry1.0, entry2.0);
1104            assert_eq!(entry1.0, 0);
1105        }
1106        for (addr1, addr2) in row1.address.iter().zip(row2.address.iter()) {
1107            assert_eq!(addr1.0, addr2.0);
1108            assert_eq!(addr1.0, 0);
1109        }
1110    }
1111
1112    #[test]
1113    fn test_frame_default() {
1114        let frame1: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1115        let frame2: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::default();
1116
1117        // Both should be equivalent
1118        assert_eq!(frame1.rows.len(), frame2.rows.len());
1119
1120        // Check that all rows are equivalent
1121        for (row1, row2) in frame1.rows.iter().zip(frame2.rows.iter()) {
1122            assert_eq!(row1, row2);
1123
1124            // Verify all entries are zero-initialized
1125            for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
1126                assert_eq!(entry1.0, entry2.0);
1127                assert_eq!(entry1.0, 0);
1128            }
1129            for (addr1, addr2) in row1.address.iter().zip(row2.address.iter()) {
1130                assert_eq!(addr1.0, addr2.0);
1131                assert_eq!(addr1.0, 0);
1132            }
1133        }
1134    }
1135
1136    #[test]
1137    fn test_dma_framebuffer_construction() {
1138        let fb = TestFrameBuffer::new();
1139        assert_eq!(fb.frames.len(), TEST_FRAME_COUNT);
1140    }
1141
1142    #[test]
1143    #[cfg(feature = "esp-hal-dma")]
1144    fn test_dma_framebuffer_dma_buffer_size() {
1145        let expected_size =
1146            core::mem::size_of::<[Frame<TEST_ROWS, TEST_COLS, TEST_NROWS>; TEST_FRAME_COUNT]>();
1147        assert_eq!(TestFrameBuffer::dma_buffer_size_bytes(), expected_size);
1148    }
1149
1150    #[test]
1151    fn test_dma_framebuffer_format() {
1152        let mut fb = TestFrameBuffer {
1153            frames: [Frame::new(); TEST_FRAME_COUNT],
1154        };
1155        fb.format();
1156
1157        // After formatting, all frames should be formatted
1158        for frame in &fb.frames {
1159            for (addr, row) in frame.rows.iter().enumerate() {
1160                for address in &row.address {
1161                    assert_eq!(address.addr() as usize, addr);
1162                }
1163            }
1164        }
1165    }
1166
1167    #[test]
1168    fn test_dma_framebuffer_set_pixel_bounds() {
1169        let mut fb = TestFrameBuffer::new();
1170
1171        // Test negative coordinates
1172        fb.set_pixel(Point::new(-1, 5), Color::RED);
1173        fb.set_pixel(Point::new(5, -1), Color::RED);
1174
1175        // Test coordinates out of bounds (should not panic)
1176        fb.set_pixel(Point::new(TEST_COLS as i32, 5), Color::RED);
1177        fb.set_pixel(Point::new(5, TEST_ROWS as i32), Color::RED);
1178    }
1179
1180    #[test]
1181    fn test_dma_framebuffer_set_pixel_internal() {
1182        let mut fb = TestFrameBuffer::new();
1183
1184        let red_color = Rgb888::new(255, 0, 0);
1185        fb.set_pixel_internal(10, 5, red_color);
1186
1187        // With 3-bit depth, brightness steps are 32 (256/8)
1188        // Frames represent thresholds: 32, 64, 96, 128, 160, 192, 224
1189        // Red value 255 should activate all frames
1190        for frame in &fb.frames {
1191            // Check upper half pixel
1192            let mapped_col_10 = map_index(10);
1193            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1194            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), false);
1195            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), false);
1196        }
1197    }
1198
1199    #[test]
1200    fn test_dma_framebuffer_brightness_modulation() {
1201        let mut fb = TestFrameBuffer::new();
1202
1203        // Test with a medium brightness value
1204        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1205        let test_brightness = brightness_step * 3; // 96
1206        let color = Rgb888::new(test_brightness, 0, 0);
1207
1208        fb.set_pixel_internal(0, 0, color);
1209
1210        // Should activate frames 0, 1, 2 (thresholds 32, 64, 96)
1211        // but not frames 3, 4, 5, 6 (thresholds 128, 160, 192, 224)
1212        for (frame_idx, frame) in fb.frames.iter().enumerate() {
1213            let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
1214            let should_be_active = test_brightness >= frame_threshold;
1215
1216            let mapped_col_0 = map_index(0);
1217            assert_eq!(frame.rows[0].data[mapped_col_0].red1(), should_be_active);
1218        }
1219    }
1220
1221    #[test]
1222    fn test_origin_dimensions() {
1223        let fb = TestFrameBuffer::new();
1224        let size = fb.size();
1225        assert_eq!(size.width, TEST_COLS as u32);
1226        assert_eq!(size.height, TEST_ROWS as u32);
1227
1228        // Test mutable reference
1229        let mut fb = TestFrameBuffer::new();
1230        let fb_ref = &mut fb;
1231        let size = fb_ref.size();
1232        assert_eq!(size.width, TEST_COLS as u32);
1233        assert_eq!(size.height, TEST_ROWS as u32);
1234    }
1235
1236    #[test]
1237    fn test_draw_target() {
1238        let mut fb = TestFrameBuffer::new();
1239
1240        let pixels = vec![
1241            embedded_graphics::Pixel(Point::new(0, 0), Color::RED),
1242            embedded_graphics::Pixel(Point::new(1, 1), Color::GREEN),
1243            embedded_graphics::Pixel(Point::new(2, 2), Color::BLUE),
1244        ];
1245
1246        let result = fb.draw_iter(pixels);
1247        assert!(result.is_ok());
1248    }
1249
1250    #[test]
1251    fn test_draw_iter_pixel_verification() {
1252        let mut fb = TestFrameBuffer::new();
1253
1254        // Create test pixels with specific colors and positions
1255        let pixels = vec![
1256            // Upper half pixels (y < NROWS) - should set color0
1257            embedded_graphics::Pixel(Point::new(5, 2), Color::RED), // (5, 2) -> red
1258            embedded_graphics::Pixel(Point::new(10, 5), Color::GREEN), // (10, 5) -> green
1259            embedded_graphics::Pixel(Point::new(15, 8), Color::BLUE), // (15, 8) -> blue
1260            embedded_graphics::Pixel(Point::new(20, 10), Color::WHITE), // (20, 10) -> white
1261            // Lower half pixels (y >= NROWS) - should set color1
1262            embedded_graphics::Pixel(Point::new(25, (TEST_NROWS + 3) as i32), Color::RED), // (25, 19) -> red
1263            embedded_graphics::Pixel(Point::new(30, (TEST_NROWS + 7) as i32), Color::GREEN), // (30, 23) -> green
1264            embedded_graphics::Pixel(Point::new(35, (TEST_NROWS + 12) as i32), Color::BLUE), // (35, 28) -> blue
1265            // Edge case: black pixel (should not be visible in first frame)
1266            embedded_graphics::Pixel(Point::new(40, 1), Color::BLACK), // (40, 1) -> black
1267            // Low brightness pixel that should not appear in first frame
1268            embedded_graphics::Pixel(Point::new(45, 3), Rgb888::new(16, 16, 16)), // Below threshold
1269        ];
1270
1271        let result = fb.draw_iter(pixels);
1272        assert!(result.is_ok());
1273
1274        // Check the first frame only
1275        let first_frame = &fb.frames[0];
1276        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1277        let first_frame_threshold = brightness_step; // 32
1278
1279        // Test upper half pixels (color0)
1280        // Red pixel at (5, 2) - should be red in first frame
1281        let col_idx = map_index(5);
1282        assert_eq!(
1283            first_frame.rows[2].data[col_idx].red1(),
1284            Color::RED.r() >= first_frame_threshold
1285        );
1286        assert_eq!(
1287            first_frame.rows[2].data[col_idx].grn1(),
1288            Color::RED.g() >= first_frame_threshold
1289        );
1290        assert_eq!(
1291            first_frame.rows[2].data[col_idx].blu1(),
1292            Color::RED.b() >= first_frame_threshold
1293        );
1294
1295        // Green pixel at (10, 5) - should be green in first frame
1296        let col_idx = map_index(10);
1297        assert_eq!(
1298            first_frame.rows[5].data[col_idx].red1(),
1299            Color::GREEN.r() >= first_frame_threshold
1300        );
1301        assert_eq!(
1302            first_frame.rows[5].data[col_idx].grn1(),
1303            Color::GREEN.g() >= first_frame_threshold
1304        );
1305        assert_eq!(
1306            first_frame.rows[5].data[col_idx].blu1(),
1307            Color::GREEN.b() >= first_frame_threshold
1308        );
1309
1310        // Blue pixel at (15, 8) - should be blue in first frame
1311        let col_idx = map_index(15);
1312        assert_eq!(
1313            first_frame.rows[8].data[col_idx].red1(),
1314            Color::BLUE.r() >= first_frame_threshold
1315        );
1316        assert_eq!(
1317            first_frame.rows[8].data[col_idx].grn1(),
1318            Color::BLUE.g() >= first_frame_threshold
1319        );
1320        assert_eq!(
1321            first_frame.rows[8].data[col_idx].blu1(),
1322            Color::BLUE.b() >= first_frame_threshold
1323        );
1324
1325        // White pixel at (20, 10) - should be white in first frame
1326        let col_idx = map_index(20);
1327        assert_eq!(
1328            first_frame.rows[10].data[col_idx].red1(),
1329            Color::WHITE.r() >= first_frame_threshold
1330        );
1331        assert_eq!(
1332            first_frame.rows[10].data[col_idx].grn1(),
1333            Color::WHITE.g() >= first_frame_threshold
1334        );
1335        assert_eq!(
1336            first_frame.rows[10].data[col_idx].blu1(),
1337            Color::WHITE.b() >= first_frame_threshold
1338        );
1339
1340        // Test lower half pixels (color1)
1341        // Red pixel at (25, TEST_NROWS + 3) -> row 3, color1
1342        let col_idx = map_index(25);
1343        assert_eq!(
1344            first_frame.rows[3].data[col_idx].red2(),
1345            Color::RED.r() >= first_frame_threshold
1346        );
1347        assert_eq!(
1348            first_frame.rows[3].data[col_idx].grn2(),
1349            Color::RED.g() >= first_frame_threshold
1350        );
1351        assert_eq!(
1352            first_frame.rows[3].data[col_idx].blu2(),
1353            Color::RED.b() >= first_frame_threshold
1354        );
1355
1356        // Green pixel at (30, TEST_NROWS + 7) -> row 7, color1
1357        let col_idx = map_index(30);
1358        assert_eq!(
1359            first_frame.rows[7].data[col_idx].red2(),
1360            Color::GREEN.r() >= first_frame_threshold
1361        );
1362        assert_eq!(
1363            first_frame.rows[7].data[col_idx].grn2(),
1364            Color::GREEN.g() >= first_frame_threshold
1365        );
1366        assert_eq!(
1367            first_frame.rows[7].data[col_idx].blu2(),
1368            Color::GREEN.b() >= first_frame_threshold
1369        );
1370
1371        // Blue pixel at (35, TEST_NROWS + 12) -> row 12, color1
1372        let col_idx = map_index(35);
1373        assert_eq!(
1374            first_frame.rows[12].data[col_idx].red2(),
1375            Color::BLUE.r() >= first_frame_threshold
1376        );
1377        assert_eq!(
1378            first_frame.rows[12].data[col_idx].grn2(),
1379            Color::BLUE.g() >= first_frame_threshold
1380        );
1381        assert_eq!(
1382            first_frame.rows[12].data[col_idx].blu2(),
1383            Color::BLUE.b() >= first_frame_threshold
1384        );
1385
1386        // Test black pixel - should not be visible in any frame
1387        let col_idx = map_index(40);
1388        assert_eq!(first_frame.rows[1].data[col_idx].red1(), false);
1389        assert_eq!(first_frame.rows[1].data[col_idx].grn1(), false);
1390        assert_eq!(first_frame.rows[1].data[col_idx].blu1(), false);
1391
1392        // Test low brightness pixel (16, 16, 16) - should not be visible in first frame (threshold 32)
1393        let col_idx = map_index(45);
1394        assert_eq!(
1395            first_frame.rows[3].data[col_idx].red1(),
1396            16 >= first_frame_threshold
1397        ); // false
1398        assert_eq!(
1399            first_frame.rows[3].data[col_idx].grn1(),
1400            16 >= first_frame_threshold
1401        ); // false
1402        assert_eq!(
1403            first_frame.rows[3].data[col_idx].blu1(),
1404            16 >= first_frame_threshold
1405        ); // false
1406    }
1407
1408    #[test]
1409    fn test_embedded_graphics_integration() {
1410        let mut fb = TestFrameBuffer::new();
1411
1412        // Draw a rectangle
1413        let result = Rectangle::new(Point::new(5, 5), Size::new(10, 8))
1414            .into_styled(PrimitiveStyle::with_fill(Color::RED))
1415            .draw(&mut fb);
1416        assert!(result.is_ok());
1417
1418        // Draw a circle
1419        let result = Circle::new(Point::new(30, 15), 8)
1420            .into_styled(PrimitiveStyle::with_fill(Color::BLUE))
1421            .draw(&mut fb);
1422        assert!(result.is_ok());
1423    }
1424
1425    #[test]
1426    fn test_read_buffer_implementation() {
1427        let fb = TestFrameBuffer::new();
1428
1429        // Test direct implementation
1430        unsafe {
1431            let (ptr, len) = fb.read_buffer();
1432            assert!(!ptr.is_null());
1433            assert_eq!(len, core::mem::size_of_val(&fb.frames));
1434        }
1435
1436        // Test mutable reference implementation
1437        let mut fb = TestFrameBuffer::new();
1438        let fb_ref = &mut fb;
1439        unsafe {
1440            let (ptr, len) = fb_ref.read_buffer();
1441            assert!(!ptr.is_null());
1442            assert_eq!(len, core::mem::size_of_val(&fb.frames));
1443        }
1444    }
1445
1446    #[test]
1447    fn test_framebuffer_trait() {
1448        let fb = TestFrameBuffer::new();
1449        assert_eq!(fb.get_word_size(), WordSize::Eight);
1450
1451        let mut fb = TestFrameBuffer::new();
1452        let fb_ref = &mut fb;
1453        assert_eq!(fb_ref.get_word_size(), WordSize::Eight);
1454    }
1455
1456    #[test]
1457    fn test_debug_formatting() {
1458        let fb = TestFrameBuffer::new();
1459        let debug_string = format!("{:?}", fb);
1460        assert!(debug_string.contains("DmaFrameBuffer"));
1461        assert!(debug_string.contains("frame_count"));
1462        assert!(debug_string.contains("frame_size"));
1463        assert!(debug_string.contains("brightness_step"));
1464    }
1465
1466    #[test]
1467    fn test_default_implementation() {
1468        let fb1 = TestFrameBuffer::new();
1469        let fb2 = TestFrameBuffer::default();
1470
1471        // Both should be equivalent
1472        assert_eq!(fb1.frames.len(), fb2.frames.len());
1473    }
1474
1475    #[cfg(feature = "esp32-ordering")]
1476    #[test]
1477    fn test_esp32_mapping() {
1478        // Test the ESP32-specific index mapping
1479        assert_eq!(map_index(0), 2);
1480        assert_eq!(map_index(1), 3);
1481        assert_eq!(map_index(2), 0);
1482        assert_eq!(map_index(3), 1);
1483        assert_eq!(map_index(4), 6); // 4 & !0b11 | 2 = 4 | 2 = 6
1484        assert_eq!(map_index(5), 7); // 5 & !0b11 | 3 = 4 | 3 = 7
1485    }
1486
1487    #[test]
1488    fn test_memory_alignment() {
1489        let fb = TestFrameBuffer::new();
1490        let ptr = &fb as *const _ as usize;
1491
1492        // Should be 4-byte aligned as specified in repr(align(4))
1493        assert_eq!(ptr % 4, 0);
1494    }
1495
1496    #[test]
1497    fn test_color_values() {
1498        let mut fb = TestFrameBuffer::new();
1499
1500        // Test different color values
1501        let colors = [
1502            (Color::RED, (255, 0, 0)),
1503            (Color::GREEN, (0, 255, 0)),
1504            (Color::BLUE, (0, 0, 255)),
1505            (Color::WHITE, (255, 255, 255)),
1506            (Color::BLACK, (0, 0, 0)),
1507        ];
1508
1509        for (i, (color, (r, g, b))) in colors.iter().enumerate() {
1510            fb.set_pixel(Point::new(i as i32, 0), *color);
1511            assert_eq!(color.r(), *r);
1512            assert_eq!(color.g(), *g);
1513            assert_eq!(color.b(), *b);
1514        }
1515    }
1516
1517    #[test]
1518    fn test_bits_assertion() {
1519        // Test that BITS <= 8 assertion is enforced at compile time
1520        // This test mainly documents the constraint
1521        assert!(TEST_BITS <= 8);
1522    }
1523
1524    #[test]
1525    #[cfg(feature = "skip-black-pixels")]
1526    fn test_skip_black_pixels_enabled() {
1527        let mut fb = TestFrameBuffer::new();
1528
1529        // Set a red pixel first
1530        fb.set_pixel_internal(10, 5, Color::RED);
1531
1532        // Verify it's red in the first frame
1533        let mapped_col_10 = map_index(10);
1534        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1535        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1536        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1537
1538        // Now set it to black - with skip-black-pixels enabled, this should be ignored
1539        fb.set_pixel_internal(10, 5, Color::BLACK);
1540
1541        // The pixel should still be red (black write was skipped)
1542        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1543        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1544        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1545    }
1546
1547    #[test]
1548    #[cfg(not(feature = "skip-black-pixels"))]
1549    fn test_skip_black_pixels_disabled() {
1550        let mut fb = TestFrameBuffer::new();
1551
1552        // Set a red pixel first
1553        fb.set_pixel_internal(10, 5, Color::RED);
1554
1555        // Verify it's red in the first frame
1556        let mapped_col_10 = map_index(10);
1557        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1558        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1559        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1560
1561        // Now set it to black - with skip-black-pixels disabled, this should overwrite
1562        fb.set_pixel_internal(10, 5, Color::BLACK);
1563
1564        // The pixel should now be black (all bits false)
1565        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), false);
1566        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1567        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1568    }
1569
1570    #[test]
1571    fn test_bcm_frame_overwrite() {
1572        let mut fb = TestFrameBuffer::new();
1573
1574        // First write a white pixel (255, 255, 255)
1575        fb.set_pixel_internal(10, 5, Color::WHITE);
1576
1577        let mapped_col_10 = map_index(10);
1578
1579        // Verify white pixel is lit in all frames (255 >= all thresholds)
1580        for frame in fb.frames.iter() {
1581            // White (255) should be active in all frames since it's >= all thresholds
1582            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1583            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), true);
1584            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), true);
1585        }
1586
1587        // Now overwrite with 50% white (128, 128, 128)
1588        let half_white = embedded_graphics::pixelcolor::Rgb888::new(128, 128, 128);
1589        fb.set_pixel_internal(10, 5, half_white);
1590
1591        // Verify only the correct frames are lit for 50% white
1592        // With 3-bit depth: thresholds are 32, 64, 96, 128, 160, 192, 224
1593        // 128 should activate frames 0, 1, 2, 3 (thresholds 32, 64, 96, 128)
1594        // but not frames 4, 5, 6 (thresholds 160, 192, 224)
1595        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1596        for (frame_idx, frame) in fb.frames.iter().enumerate() {
1597            let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
1598            let should_be_active = 128 >= frame_threshold;
1599
1600            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), should_be_active);
1601            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), should_be_active);
1602            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), should_be_active);
1603        }
1604
1605        // Specifically verify the expected pattern for 3-bit depth
1606        // Frames 0-3 should be active (thresholds 32, 64, 96, 128)
1607        for frame_idx in 0..4 {
1608            assert_eq!(
1609                fb.frames[frame_idx].rows[5].data[mapped_col_10].red1(),
1610                true
1611            );
1612        }
1613        // Frames 4-6 should be inactive (thresholds 160, 192, 224)
1614        for frame_idx in 4..TEST_FRAME_COUNT {
1615            assert_eq!(
1616                fb.frames[frame_idx].rows[5].data[mapped_col_10].red1(),
1617                false
1618            );
1619        }
1620    }
1621
1622    #[test]
1623    fn test_new_auto_formats() {
1624        let fb = TestFrameBuffer::new();
1625
1626        // After new(), all frames should be formatted
1627        for frame in &fb.frames {
1628            for (addr, row) in frame.rows.iter().enumerate() {
1629                for address in &row.address {
1630                    assert_eq!(address.addr() as usize, addr);
1631                }
1632            }
1633        }
1634    }
1635
1636    #[test]
1637    fn test_erase() {
1638        let mut fb = TestFrameBuffer::new();
1639
1640        // Set some pixels
1641        fb.set_pixel_internal(10, 5, Color::RED);
1642        fb.set_pixel_internal(20, 10, Color::GREEN);
1643
1644        let mapped_col_10 = map_index(10);
1645        let mapped_col_20 = map_index(20);
1646
1647        // Verify pixels are set
1648        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1649        assert_eq!(fb.frames[0].rows[10].data[mapped_col_20].grn1(), true);
1650
1651        // erase
1652        fb.erase();
1653
1654        // Verify pixels are cleared but control bits are preserved
1655        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), false);
1656        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1657        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1658        assert_eq!(fb.frames[0].rows[10].data[mapped_col_20].red1(), false);
1659        assert_eq!(fb.frames[0].rows[10].data[mapped_col_20].grn1(), false);
1660        assert_eq!(fb.frames[0].rows[10].data[mapped_col_20].blu1(), false);
1661
1662        // Verify control bits are still correct
1663        for frame in &fb.frames {
1664            for (addr, row) in frame.rows.iter().enumerate() {
1665                // Check address words
1666                for address in &row.address {
1667                    assert_eq!(address.addr() as usize, addr);
1668                }
1669                // Check OE bits in data - should be exactly one false (for last logical column)
1670                let oe_false_count = row
1671                    .data
1672                    .iter()
1673                    .filter(|entry| !entry.output_enable())
1674                    .count();
1675                assert_eq!(oe_false_count, 1);
1676            }
1677        }
1678    }
1679
1680    #[test]
1681    fn test_row_clear_colors() {
1682        let mut row: Row<TEST_COLS> = Row::new();
1683        row.format(5);
1684
1685        // Set some colors
1686        row.set_color0(0, true, false, true);
1687        row.set_color1(1, false, true, false);
1688
1689        let mapped_col_0 = map_index(0);
1690        let mapped_col_1 = map_index(1);
1691
1692        // Verify colors are set
1693        assert_eq!(row.data[mapped_col_0].red1(), true);
1694        assert_eq!(row.data[mapped_col_0].blu1(), true);
1695        assert_eq!(row.data[mapped_col_1].grn2(), true);
1696
1697        // Store original control bits
1698        let original_oe_0 = row.data[mapped_col_0].output_enable();
1699        let original_latch_0 = row.data[mapped_col_0].latch();
1700        let original_oe_1 = row.data[mapped_col_1].output_enable();
1701        let original_latch_1 = row.data[mapped_col_1].latch();
1702
1703        // Clear colors
1704        row.clear_colors();
1705
1706        // Verify colors are cleared
1707        assert_eq!(row.data[mapped_col_0].red1(), false);
1708        assert_eq!(row.data[mapped_col_0].grn1(), false);
1709        assert_eq!(row.data[mapped_col_0].blu1(), false);
1710        assert_eq!(row.data[mapped_col_1].red2(), false);
1711        assert_eq!(row.data[mapped_col_1].grn2(), false);
1712        assert_eq!(row.data[mapped_col_1].blu2(), false);
1713
1714        // Verify control bits are preserved
1715        assert_eq!(row.data[mapped_col_0].output_enable(), original_oe_0);
1716        assert_eq!(row.data[mapped_col_0].latch(), original_latch_0);
1717        assert_eq!(row.data[mapped_col_1].output_enable(), original_oe_1);
1718        assert_eq!(row.data[mapped_col_1].latch(), original_latch_1);
1719    }
1720
1721    #[test]
1722    fn test_make_addr_table_function() {
1723        // Test the make_addr_table function directly to ensure code coverage
1724        let table = make_addr_table();
1725
1726        // Verify basic properties of the generated table
1727        assert_eq!(table.len(), 32); // Should have 32 address entries (0-31)
1728
1729        // Check first address (0)
1730        let addr_0 = &table[0];
1731        assert_eq!(addr_0.len(), 4); // Should have 4 address words
1732
1733        // Verify that exactly one address word has latch=false (index 3 in logical order)
1734        let latch_false_count = addr_0.iter().filter(|addr| !addr.latch()).count();
1735        assert_eq!(latch_false_count, 1);
1736
1737        // All addresses should have addr field set to 0 for the first entry
1738        for addr in addr_0 {
1739            assert_eq!(addr.addr(), 0);
1740        }
1741
1742        // Check last address (31)
1743        let addr_31 = &table[31];
1744        let latch_false_count = addr_31.iter().filter(|addr| !addr.latch()).count();
1745        assert_eq!(latch_false_count, 1);
1746
1747        // All addresses should have addr field set to 31 for the last entry
1748        for addr in addr_31 {
1749            assert_eq!(addr.addr(), 31);
1750        }
1751    }
1752
1753    #[test]
1754    fn test_make_data_template_function() {
1755        // Test the make_data_template function directly to ensure code coverage
1756        let template = make_data_template::<TEST_COLS>();
1757
1758        // Verify basic properties
1759        assert_eq!(template.len(), TEST_COLS);
1760
1761        // All entries should have latch=false
1762        for entry in &template {
1763            assert_eq!(entry.latch(), false);
1764        }
1765
1766        // Exactly one entry should have output_enable=false (the last logical column)
1767        let oe_false_count = template
1768            .iter()
1769            .filter(|entry| !entry.output_enable())
1770            .count();
1771        assert_eq!(oe_false_count, 1);
1772
1773        // Test with a small template size to verify edge cases
1774        let small_template = make_data_template::<4>();
1775        assert_eq!(small_template.len(), 4);
1776
1777        let oe_false_count = small_template
1778            .iter()
1779            .filter(|entry| !entry.output_enable())
1780            .count();
1781        assert_eq!(oe_false_count, 1);
1782
1783        // Test with single column - but skip this test if ESP32 ordering is enabled
1784        // because the mapping function assumes at least 4 columns for proper mapping
1785        #[cfg(not(feature = "esp32-ordering"))]
1786        {
1787            let single_template = make_data_template::<1>();
1788            assert_eq!(single_template.len(), 1);
1789            assert_eq!(single_template[0].output_enable(), false); // Single column should have OE=false
1790            assert_eq!(single_template[0].latch(), false);
1791        }
1792    }
1793
1794    #[test]
1795    fn test_addr_table_correctness() {
1796        // Test that the pre-computed address table matches the original logic
1797        for addr in 0..32 {
1798            let mut expected_addresses = [Address::new(); 4];
1799
1800            // Original logic
1801            for i in 0..4 {
1802                let latch = !matches!(i, 3);
1803                #[cfg(feature = "esp32-ordering")]
1804                let mapped_i = map_index(i);
1805                #[cfg(not(feature = "esp32-ordering"))]
1806                let mapped_i = i;
1807
1808                expected_addresses[mapped_i].set_latch(latch);
1809                expected_addresses[mapped_i].set_addr(addr);
1810            }
1811
1812            // Compare with table
1813            let table_addresses = &ADDR_TABLE[addr as usize];
1814            for i in 0..4 {
1815                assert_eq!(table_addresses[i].0, expected_addresses[i].0);
1816            }
1817        }
1818    }
1819
1820    // Helper constants for the glyph dimensions used by FONT_6X10
1821    const CHAR_W: i32 = 6;
1822    const CHAR_H: i32 = 10;
1823
1824    /// Draws the glyph 'A' at `origin` and verifies every pixel against a software reference.
1825    /// Re-usable for different panel locations.
1826    fn verify_glyph_at(fb: &mut TestFrameBuffer, origin: Point) {
1827        use embedded_graphics::mock_display::MockDisplay;
1828        use embedded_graphics::mono_font::ascii::FONT_6X10;
1829        use embedded_graphics::mono_font::MonoTextStyle;
1830        use embedded_graphics::text::{Baseline, Text};
1831
1832        // Draw the character on the framebuffer.
1833        let style = MonoTextStyle::new(&FONT_6X10, Color::WHITE);
1834        Text::with_baseline("A", origin, style, Baseline::Top)
1835            .draw(fb)
1836            .unwrap();
1837
1838        // Reference bitmap for the glyph at (0,0)
1839        let mut reference: MockDisplay<Color> = MockDisplay::new();
1840        Text::with_baseline("A", Point::zero(), style, Baseline::Top)
1841            .draw(&mut reference)
1842            .unwrap();
1843
1844        // Iterate over the glyph's bounding box and compare pixel states.
1845        for dy in 0..CHAR_H {
1846            for dx in 0..CHAR_W {
1847                let expected_on = reference
1848                    .get_pixel(Point::new(dx, dy))
1849                    .unwrap_or(Color::BLACK)
1850                    != Color::BLACK;
1851
1852                let gx = (origin.x + dx) as usize;
1853                let gy = (origin.y + dy) as usize;
1854
1855                // we have computed the origin to be within the panel, so we don't need to check for bounds
1856                // if gx >= TEST_COLS || gy >= TEST_ROWS {
1857                //     continue;
1858                // }
1859
1860                // Fetch the entry from frame 0 directly.
1861                let frame0 = &fb.frames[0];
1862                let e = if gy < TEST_NROWS {
1863                    &frame0.rows[gy].data[map_index(gx)]
1864                } else {
1865                    &frame0.rows[gy - TEST_NROWS].data[map_index(gx)]
1866                };
1867
1868                let (r, g, b) = if gy >= TEST_NROWS {
1869                    (e.red2(), e.grn2(), e.blu2())
1870                } else {
1871                    (e.red1(), e.grn1(), e.blu1())
1872                };
1873
1874                if expected_on {
1875                    assert!(r && g && b);
1876                } else {
1877                    assert!(!r && !g && !b);
1878                }
1879            }
1880        }
1881    }
1882
1883    #[test]
1884    fn test_draw_char_corners() {
1885        // Upper-left and lower-right character placement.
1886        let upper_left = Point::new(0, 0);
1887        let lower_right = Point::new(TEST_COLS as i32 - CHAR_W, TEST_ROWS as i32 - CHAR_H);
1888
1889        let mut fb = TestFrameBuffer::new();
1890
1891        // Verify glyph in the upper-left corner.
1892        verify_glyph_at(&mut fb, upper_left);
1893        // Verify glyph in the lower-right corner.
1894        verify_glyph_at(&mut fb, lower_right);
1895    }
1896
1897    #[test]
1898    fn test_framebuffer_operations_trait_erase() {
1899        let mut fb = TestFrameBuffer::new();
1900
1901        // Set a couple of pixels so erase has an effect to clear
1902        fb.set_pixel_internal(10, 5, Color::RED);
1903        fb.set_pixel_internal(20, 10, Color::GREEN);
1904
1905        // Call the trait method explicitly to exercise the impl
1906        <TestFrameBuffer as FrameBufferOperations<
1907            TEST_ROWS,
1908            TEST_COLS,
1909            TEST_NROWS,
1910            TEST_BITS,
1911            TEST_FRAME_COUNT,
1912        >>::erase(&mut fb);
1913
1914        // Verify colors are cleared but control bits/timing remain intact on frame 0
1915        let mc10 = map_index(10);
1916        let mc20 = map_index(20);
1917        assert_eq!(fb.frames[0].rows[5].data[mc10].red1(), false);
1918        assert_eq!(fb.frames[0].rows[10].data[mc20].grn1(), false);
1919
1920        // Data entries should still have the same OE pattern and latch should remain false for all
1921        let row0 = &fb.frames[0].rows[0];
1922        let oe_false_count = row0
1923            .data
1924            .iter()
1925            .filter(|entry| !entry.output_enable())
1926            .count();
1927        assert_eq!(oe_false_count, 1);
1928        assert!(row0.data.iter().all(|e| !e.latch()));
1929
1930        // Address words should remain precomputed table values
1931        for (i, addr) in row0.address.iter().enumerate() {
1932            assert_eq!(addr.0, ADDR_TABLE[0][i].0);
1933        }
1934    }
1935
1936    #[test]
1937    fn test_framebuffer_operations_trait_set_pixel() {
1938        let mut fb = TestFrameBuffer::new();
1939
1940        // Call the trait method explicitly to exercise the impl
1941        <TestFrameBuffer as FrameBufferOperations<
1942            TEST_ROWS,
1943            TEST_COLS,
1944            TEST_NROWS,
1945            TEST_BITS,
1946            TEST_FRAME_COUNT,
1947        >>::set_pixel(&mut fb, Point::new(8, 3), Color::BLUE);
1948
1949        // For BITS=3, BLUE should light blue channel in early frames
1950        let idx = map_index(8);
1951        assert_eq!(fb.frames[0].rows[3].data[idx].blu1(), true);
1952        // Red/Green should be off for BLUE at frame 0
1953        assert_eq!(fb.frames[0].rows[3].data[idx].red1(), false);
1954        assert_eq!(fb.frames[0].rows[3].data[idx].grn1(), false);
1955    }
1956}