Skip to main content

hub75_framebuffer/
plain.rs

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