Skip to main content

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