Skip to main content

hub75_framebuffer/bitplane/plain/
frame.rs

1//! Plane-major bitplane framebuffer.
2//!
3//! Stores all rows for one bit-plane contiguously, then the next plane, etc.
4//! Memory scales linearly with `PLANES`: the buffer contains `PLANES` copies
5//! of the row data (one per bit-plane).
6//!
7//! Each row is `COLS` 16-bit entries plus an optional inter-row gap, so total
8//! size is approximately `PLANES × NROWS × (COLS + INTER_ROW_BLANK) × 2`
9//! bytes.
10//!
11//! Planes are stored **LSB-first**: plane 0 carries the least-significant
12//! bit and is displayed once per frame; plane `PLANES-1` carries the MSB and
13//! is displayed `2^(PLANES-1)` times per frame.
14//!
15//! # DMA Descriptor Pattern
16//!
17//! The planes are contiguous (`repr(C)`), so each BCM segment streams a
18//! whole *suffix* of planes: the segment for plane `k` starts at plane `k`
19//! and covers planes `k..PLANES`, repeated just enough times to bring plane
20//! `k`'s total coverage to `2^k` displays per frame. This halves the number
21//! of DMA transfers per frame — `2^(PLANES-1)` instead of `2^PLANES - 1` —
22//! while the streamed bytes, and therefore the brightness, are unchanged.
23//! The inter-row gap (embedded in each row) and the optional tail word
24//! (embedded in each plane) travel inside the coalesced segments
25//! automatically.
26//!
27//! ```text
28//! planes 0..PLANES      × 1 rep        → planes[0..]
29//! planes 1..PLANES      × 1 rep        → planes[1..]
30//! planes 2..PLANES      × 2 reps       → planes[2..]
31//! …
32//! plane  PLANES-1 (MSB) × 2^(PLANES-2) → planes[PLANES-1]
33//! ```
34
35use core::convert::Infallible;
36
37use embedded_graphics::pixelcolor::RgbColor;
38use embedded_graphics::prelude::{DrawTarget, OriginDimensions, Point, Size};
39
40use crate::Color;
41use crate::{map_row_index, slot_addresses};
42use crate::{BcmLenReps, FrameBuffer, BCM_SEQUENCE_CAPACITY};
43use crate::{FrameBufferOperations, MutableFrameBuffer};
44
45use super::{make_data_template, map_index, Entry, INTER_ROW_BLANK, OE_BLANK};
46
47#[derive(Clone, Copy, PartialEq, Debug)]
48#[repr(C)]
49/// A single BCM row payload for 16-bit plain output.
50///
51/// Row addressing, latch, OE, and pixel colour data are all encoded into the
52/// 16-bit `Entry` words -- no separate address bytes are needed.
53pub struct Row<const COLS: usize> {
54    pub(crate) data: [Entry; COLS],
55    pub(crate) gap: [Entry; INTER_ROW_BLANK],
56}
57
58impl<const COLS: usize> Row<COLS> {
59    /// Creates a zero-initialized row.
60    ///
61    /// Call [`Self::format`] before first use to populate row control metadata.
62    #[must_use]
63    pub const fn new() -> Self {
64        Self {
65            data: [Entry::new(); COLS],
66            gap: [Entry::new(); INTER_ROW_BLANK],
67        }
68    }
69
70    /// Formats this row for the provided multiplexed row address.
71    ///
72    /// `prev_addr` is the *previous* scan row's address: it is driven onto
73    /// the address lines while this row's pixels are shifted in, so the
74    /// panel keeps displaying the previously latched row during the shift.
75    ///
76    /// Sets up blanking delay, output-enable, latch, and address bits in the
77    /// pixel stream template.
78    #[inline]
79    pub const fn format(&mut self, prev_addr: u8) {
80        let template = make_data_template::<COLS>(prev_addr);
81        let mut i = 0;
82        while i < COLS {
83            self.data[i] = template[i];
84            i += 1;
85        }
86
87        let gap_val = (prev_addr as u16) | OE_BLANK;
88        let mut i = 0;
89        // INTER_ROW_BLANK is 0 unless an inter-row-blank-* feature is enabled.
90        #[allow(clippy::absurd_extreme_comparisons)]
91        while i < INTER_ROW_BLANK {
92            self.gap[i].0 = gap_val;
93            i += 1;
94        }
95    }
96}
97
98impl<const COLS: usize> Default for Row<COLS> {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104/// A single bit-plane's DMA payload: all rows for the plane, optionally
105/// followed by a tail word (see the `tail-closes-latch` feature).
106#[derive(Clone, Copy, PartialEq, Debug)]
107#[repr(C)]
108pub struct PlaneData<const NROWS: usize, const COLS: usize> {
109    pub(crate) rows: [Row<COLS>; NROWS],
110    #[cfg(all(feature = "tail-closes-latch", feature = "esp32-ordering"))]
111    pub(crate) padding: Entry,
112    #[cfg(feature = "tail-closes-latch")]
113    pub(crate) tail: Entry,
114    #[cfg(all(not(feature = "esp32-ordering"), feature = "tail-closes-latch"))]
115    pub(crate) padding: Entry,
116}
117
118impl<const NROWS: usize, const COLS: usize> PlaneData<NROWS, COLS> {
119    const fn new() -> Self {
120        Self {
121            rows: [Row::new(); NROWS],
122            #[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
123            padding: Entry::new(),
124            #[cfg(feature = "tail-closes-latch")]
125            tail: Entry::new(),
126            #[cfg(all(not(feature = "esp32-ordering"), feature = "tail-closes-latch"))]
127            padding: Entry::new(),
128        }
129    }
130}
131
132/// Coalesced BCM suffix for plane `plane_idx`: returns
133/// `(covered_planes, reps)` — how many planes the segment spans (starting at
134/// `plane_idx`) and how many times that suffix is streamed. See the
135/// module-level "DMA Descriptor Pattern" section for the full scheme.
136const fn plane_suffix(plane_idx: usize, planes: usize) -> (usize, usize) {
137    let covered = planes - plane_idx;
138    let reps = if plane_idx == 0 {
139        1
140    } else {
141        1 << (plane_idx - 1)
142    };
143    (covered, reps)
144}
145
146/// BCM sequence for one complete frame: one entry per plane, each streaming
147/// the contiguous plane suffix `plane..PLANES`. Entries past `PLANES` are
148/// zero padding.
149const fn bcm_sequence<const NROWS: usize, const COLS: usize, const PLANES: usize>(
150) -> [BcmLenReps; BCM_SEQUENCE_CAPACITY] {
151    assert!(PLANES <= BCM_SEQUENCE_CAPACITY);
152    let plane_bytes = core::mem::size_of::<PlaneData<NROWS, COLS>>();
153    let mut seq = [BcmLenReps::ZERO; BCM_SEQUENCE_CAPACITY];
154    let mut plane = 0usize;
155    while plane < PLANES {
156        let (covered, reps) = plane_suffix(plane, PLANES);
157        seq[plane] = BcmLenReps {
158            len: plane_bytes * covered,
159            reps,
160        };
161        plane += 1;
162    }
163    seq
164}
165
166/// Plane-major BCM framebuffer (per-plane storage).
167#[derive(Copy, Clone)]
168#[repr(C, align(4))]
169pub struct DmaFrameBuffer<const NROWS: usize, const COLS: usize, const PLANES: usize> {
170    pub(crate) planes: [PlaneData<NROWS, COLS>; PLANES],
171}
172
173impl<const NROWS: usize, const COLS: usize, const PLANES: usize>
174    DmaFrameBuffer<NROWS, COLS, PLANES>
175{
176    /// Creates a new frame buffer, pre-formatted and ready for use.
177    ///
178    /// # Panics
179    /// Panics if `NROWS` is not within `1..=32` (5-bit row address) or
180    /// `PLANES` is not within `1..=8` (8-bit color depth). With the
181    /// `esp32-ordering` feature, also panics if `COLS` is not even (the
182    /// ESP32's byte-order swap requires an even column count). In const
183    /// contexts (e.g. `static` framebuffers) this is a compile-time error.
184    #[must_use]
185    pub const fn new() -> Self {
186        assert!(NROWS >= 1 && NROWS <= 32, "NROWS must be within 1..=32");
187        assert!(PLANES >= 1 && PLANES <= 8, "PLANES must be within 1..=8");
188        #[cfg(feature = "esp32-ordering")]
189        assert!(
190            COLS % 2 == 0,
191            "esp32-ordering feature requires COLS to be even"
192        );
193        let mut instance = Self {
194            planes: [PlaneData::new(); PLANES],
195        };
196        instance.format();
197        instance
198    }
199
200    /// Returns the number of BCM chunks (one per bit-plane).
201    #[must_use]
202    #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
203    pub const fn bcm_chunk_count() -> usize {
204        PLANES
205    }
206
207    /// Returns the byte size of one BCM chunk (a single bit-plane including tail word).
208    #[must_use]
209    #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
210    pub const fn bcm_chunk_bytes() -> usize {
211        core::mem::size_of::<PlaneData<NROWS, COLS>>()
212    }
213
214    /// Formats the frame buffer with row addresses and control bits.
215    #[inline]
216    pub const fn format(&mut self) {
217        let mut p = 0;
218        while p < PLANES {
219            let mut slot = 0;
220            while slot < NROWS {
221                let (prev_addr, _) = slot_addresses::<NROWS>(slot);
222                self.planes[p].rows[slot].format(prev_addr);
223                slot += 1;
224            }
225            #[cfg(feature = "tail-closes-latch")]
226            {
227                self.planes[p].tail.0 = 0x1f | OE_BLANK;
228                self.planes[p].padding.0 = 0x1f | OE_BLANK;
229            }
230            p += 1;
231        }
232    }
233
234    /// Erase pixel colors while preserving row control data.
235    #[inline]
236    pub fn erase(&mut self) {
237        const MASK: u16 = !0b0111_1110_0000_0000; // clear bits 9-14 (R1,G1,B1,R2,G2,B2)
238        for plane in &mut self.planes {
239            for row in &mut plane.rows {
240                for entry in &mut row.data {
241                    entry.0 &= MASK;
242                }
243            }
244        }
245    }
246
247    /// Set a pixel in the framebuffer.
248    #[inline]
249    pub fn set_pixel(&mut self, p: Point, color: Color) {
250        if p.x < 0 || p.y < 0 {
251            return;
252        }
253        self.set_pixel_internal(p.x as usize, p.y as usize, color);
254    }
255
256    #[inline]
257    fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
258        if x >= COLS || y >= NROWS * 2 {
259            return;
260        }
261
262        // Early exit for black pixels - common in UI backgrounds
263        // Only enabled when skip-black-pixels feature is active
264        #[cfg(feature = "skip-black-pixels")]
265        if color == Color::BLACK {
266            return;
267        }
268
269        let row_idx = map_row_index::<NROWS>(if y < NROWS { y } else { y - NROWS });
270        let is_top = y < NROWS;
271        let red = color.r();
272        let green = color.g();
273        let blue = color.b();
274
275        for plane_idx in 0..PLANES {
276            let bit = (8 - PLANES + plane_idx) as u32;
277            let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
278                | ((u8::from(((green >> bit) & 1) != 0)) << 1)
279                | u8::from(((red >> bit) & 1) != 0);
280            let col_idx = map_index(x);
281            let entry = &mut self.planes[plane_idx].rows[row_idx].data[col_idx];
282            if is_top {
283                entry.set_color0_bits(bits);
284            } else {
285                entry.set_color1_bits(bits);
286            }
287        }
288    }
289}
290
291impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
292    for DmaFrameBuffer<NROWS, COLS, PLANES>
293{
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
300    for DmaFrameBuffer<NROWS, COLS, PLANES>
301{
302    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
303        f.debug_struct("DmaFrameBuffer")
304            .field("size", &core::mem::size_of_val(&self.planes))
305            .field("plane_count", &self.planes.len())
306            .field("plane_size", &core::mem::size_of_val(&self.planes[0]))
307            .finish()
308    }
309}
310
311#[cfg(feature = "defmt")]
312impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
313    for DmaFrameBuffer<NROWS, COLS, PLANES>
314{
315    fn format(&self, f: defmt::Formatter) {
316        defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
317        defmt::write!(f, " size: {}", core::mem::size_of_val(&self.planes));
318        defmt::write!(
319            f,
320            " plane_size: {}",
321            core::mem::size_of_val(&self.planes[0])
322        );
323    }
324}
325
326impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
327    for DmaFrameBuffer<NROWS, COLS, PLANES>
328{
329    type Word = u16;
330
331    const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY] = bcm_sequence::<NROWS, COLS, PLANES>();
332
333    const BCM_SEQUENCE_LEN: usize = PLANES;
334
335    const BCM_SEQUENCE_COUNT: usize = 1;
336
337    fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
338        assert!(
339            index < PLANES,
340            "segment index {index} out of range for {PLANES} planes"
341        );
342        (&raw const self.planes[index]).cast::<u8>()
343    }
344}
345
346impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
347    for DmaFrameBuffer<NROWS, COLS, PLANES>
348{
349    #[inline]
350    fn erase(&mut self) {
351        DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
352    }
353
354    #[inline]
355    fn set_pixel(&mut self, p: Point, color: Color) {
356        DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
357    }
358}
359
360impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
361    for DmaFrameBuffer<NROWS, COLS, PLANES>
362{
363}
364
365impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
366    for DmaFrameBuffer<NROWS, COLS, PLANES>
367{
368    fn size(&self) -> Size {
369        Size::new(COLS as u32, (NROWS * 2) as u32)
370    }
371}
372
373impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
374    for DmaFrameBuffer<NROWS, COLS, PLANES>
375{
376    type Color = Color;
377    type Error = Infallible;
378
379    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
380    where
381        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
382    {
383        for pixel in pixels {
384            if pixel.0.x < 0 || pixel.0.y < 0 {
385                continue;
386            }
387            self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
388        }
389        Ok(())
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    extern crate std;
396
397    use super::super::{LEAD_BLANK_DELAY, TRAIL_BLANK_DELAY};
398    use super::*;
399    use embedded_graphics::prelude::*;
400    use std::format;
401
402    const TEST_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
403        128
404    } else {
405        64
406    };
407    type TestBuffer = DmaFrameBuffer<16, TEST_COLS, 8>;
408
409    #[test]
410    fn row_format_sets_address_and_control_bits() {
411        let mut row = Row::<TEST_COLS>::new();
412        row.format(4);
413
414        let last_idx = map_index(TEST_COLS - 1);
415        assert_eq!(row.data[last_idx].latch(), true);
416        assert_eq!(row.data[last_idx].addr(), 4);
417
418        let first_idx = map_index(0);
419        assert_eq!(row.data[first_idx].addr(), 4);
420        assert_eq!(row.data[first_idx].latch(), false);
421    }
422
423    #[test]
424    fn format_sets_expected_row_addresses_for_all_rows() {
425        let mut fb = TestBuffer::new();
426        fb.format();
427
428        for plane_idx in 0..8 {
429            for slot in 0..16 {
430                let (prev_addr, _) = slot_addresses::<16>(slot);
431                let last_col = map_index(TEST_COLS - 1);
432                assert_eq!(
433                    fb.planes[plane_idx].rows[slot].data[last_col].addr(),
434                    u16::from(prev_addr)
435                );
436                assert_eq!(fb.planes[plane_idx].rows[slot].data[last_col].latch(), true);
437            }
438        }
439    }
440
441    #[test]
442    fn set_pixel_maps_top_half_bits_per_plane() {
443        let mut fb = TestBuffer::new();
444        let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
445        fb.set_pixel(Point::new(2, 3), color);
446
447        for plane_idx in 0..8 {
448            let bit = plane_idx;
449            let entry = fb.planes[plane_idx].rows[map_row_index::<16>(3)].data[map_index(2)];
450            assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
451            assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
452            assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
453        }
454    }
455
456    #[test]
457    fn set_pixel_maps_bottom_half_bits_per_plane() {
458        let mut fb = TestBuffer::new();
459        let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
460        fb.set_pixel(Point::new(4, 20), color);
461
462        for plane_idx in 0..8 {
463            let bit = plane_idx;
464            let entry = fb.planes[plane_idx].rows[map_row_index::<16>(4)].data[map_index(4)];
465            assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
466            assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
467            assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
468        }
469    }
470
471    #[test]
472    #[cfg(feature = "reverse-row-order")]
473    fn reverse_row_order_stores_rows_back_to_front() {
474        let mut fb = TestBuffer::new();
475
476        // Slot 0 is streamed first and renders the last panel row; while it
477        // is shifted the panel still displays row 0 (latched by the previous
478        // frame's final slot), so its entries carry prev_addr 0.
479        assert_eq!(fb.planes[0].rows[0].data[map_index(0)].addr(), 0);
480        // The final slot renders panel row 0 and carries prev_addr 1.
481        assert_eq!(fb.planes[0].rows[15].data[map_index(0)].addr(), 1);
482
483        // Logical row 0 maps to the last memory slot.
484        fb.set_pixel(Point::new(2, 0), Color::RED);
485        let col2 = map_index(2);
486        assert!(fb.planes[7].rows[15].data[col2].red1());
487        assert!(!fb.planes[7].rows[0].data[col2].red1());
488    }
489
490    #[test]
491    fn erase_clears_only_color_bits() {
492        let mut fb = TestBuffer::new();
493        let oe_before =
494            fb.planes[0].rows[map_row_index::<16>(0)].data[map_index(1)].output_enable();
495        fb.set_pixel(Point::new(0, 0), Color::WHITE);
496        fb.erase();
497
498        for plane in &fb.planes {
499            for row in &plane.rows {
500                for entry in &row.data {
501                    assert!(!entry.red1());
502                    assert!(!entry.grn1());
503                    assert!(!entry.blu1());
504                    assert!(!entry.red2());
505                    assert!(!entry.grn2());
506                    assert!(!entry.blu2());
507                }
508            }
509        }
510
511        assert_eq!(
512            fb.planes[0].rows[map_row_index::<16>(0)].data[map_index(1)].output_enable(),
513            oe_before
514        );
515    }
516
517    #[test]
518    fn draw_target_iter_sets_pixels() {
519        let mut fb = TestBuffer::new();
520        let pixels = [Pixel(Point::new(1, 1), Color::RED)];
521        let result = fb.draw_iter(pixels);
522        assert!(result.is_ok());
523
524        for plane_idx in 0..8 {
525            let bit = plane_idx;
526            let entry = fb.planes[plane_idx].rows[map_row_index::<16>(1)].data[map_index(1)];
527            assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
528            assert!(!entry.grn1());
529            assert!(!entry.blu1());
530        }
531    }
532
533    #[test]
534    fn set_pixel_ignores_out_of_bounds_and_negative() {
535        let mut fb = TestBuffer::new();
536        let before = fb.planes;
537        fb.set_pixel(Point::new(-1, 0), Color::WHITE);
538        fb.set_pixel(Point::new(0, -1), Color::WHITE);
539        fb.set_pixel(Point::new(TEST_COLS as i32, 0), Color::WHITE);
540        fb.set_pixel(Point::new(0, 32), Color::WHITE);
541        assert_eq!(fb.planes, before);
542    }
543
544    #[test]
545    #[cfg(feature = "skip-black-pixels")]
546    fn test_skip_black_pixels_enabled() {
547        let mut fb = TestBuffer::new();
548
549        // Set a red pixel first
550        fb.set_pixel_internal(10, 5, Color::RED);
551
552        // Verify it's red in the first plane
553        let mapped_col_10 = map_index(10);
554        assert!(fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].red1());
555        assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].grn1());
556        assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].blu1());
557
558        // Now set it to black - with skip-black-pixels enabled, this should be ignored
559        fb.set_pixel_internal(10, 5, Color::BLACK);
560
561        // The pixel should still be red (black write was skipped)
562        assert!(fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].red1());
563        assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].grn1());
564        assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].blu1());
565    }
566
567    #[test]
568    #[cfg(not(feature = "skip-black-pixels"))]
569    fn test_skip_black_pixels_disabled() {
570        let mut fb = TestBuffer::new();
571
572        // Set a red pixel first
573        fb.set_pixel_internal(10, 5, Color::RED);
574
575        // Verify it's red in the first plane
576        let mapped_col_10 = map_index(10);
577        assert!(fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].red1());
578        assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].grn1());
579        assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].blu1());
580
581        // Now set it to black - with skip-black-pixels disabled, this should overwrite
582        fb.set_pixel_internal(10, 5, Color::BLACK);
583
584        // The pixel should now be black (all bits false)
585        assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].red1());
586        assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].grn1());
587        assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].blu1());
588    }
589
590    #[test]
591    #[should_panic(expected = "NROWS must be within 1..=32")]
592    fn new_panics_for_too_many_rows() {
593        let _ = DmaFrameBuffer::<33, TEST_COLS, 8>::new();
594    }
595
596    #[test]
597    #[should_panic(expected = "PLANES must be within 1..=8")]
598    fn new_panics_for_too_many_planes() {
599        let _ = DmaFrameBuffer::<16, TEST_COLS, 9>::new();
600    }
601
602    #[test]
603    #[allow(deprecated)]
604    fn bcm_chunk_info_for_common_panel() {
605        assert_eq!(TestBuffer::bcm_chunk_count(), 8);
606        assert_eq!(
607            TestBuffer::bcm_chunk_bytes(),
608            core::mem::size_of::<PlaneData<16, TEST_COLS>>()
609        );
610    }
611
612    #[test]
613    fn origin_dimensions_match_panel_geometry() {
614        let fb = TestBuffer::new();
615        assert_eq!(fb.size(), Size::new(TEST_COLS as u32, 32));
616    }
617
618    #[test]
619    fn debug_impl_includes_shape_information() {
620        let fb = TestBuffer::new();
621        let s = format!("{fb:?}");
622        assert!(s.contains("DmaFrameBuffer"));
623        assert!(s.contains("plane_count"));
624        assert!(s.contains("plane_size"));
625    }
626
627    #[test]
628    fn row_format_sets_expected_blank_and_latch_positions() {
629        let mut row = Row::<TEST_COLS>::new();
630        row.format(4);
631
632        let oe_active = cfg!(feature = "invert-oe");
633
634        let idx_active = map_index(TRAIL_BLANK_DELAY);
635        assert_eq!(row.data[idx_active].output_enable(), oe_active);
636        assert_eq!(row.data[idx_active].addr(), 4);
637
638        let idx_blank = map_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
639        assert_eq!(row.data[idx_blank].output_enable(), !oe_active);
640
641        let idx_last = map_index(TEST_COLS - 1);
642        assert!(row.data[idx_last].latch());
643        assert_eq!(row.data[idx_last].addr(), 4);
644    }
645
646    #[test]
647    fn default_constructors_match_new() {
648        let row_default = Row::<TEST_COLS>::default();
649        let row_new = Row::<TEST_COLS>::new();
650        assert_eq!(row_default, row_new);
651
652        let fb_default = TestBuffer::default();
653        let fb_new = TestBuffer::new();
654        assert_eq!(fb_default.planes, fb_new.planes);
655    }
656
657    #[test]
658    fn framebuffer_operations_trait_delegates_correctly() {
659        let mut fb = TestBuffer::new();
660        FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
661
662        assert!(fb.planes[0].rows[map_row_index::<16>(5)].data[map_index(3)].grn1());
663
664        FrameBufferOperations::erase(&mut fb);
665        for plane in &fb.planes {
666            for row in &plane.rows {
667                for entry in &row.data {
668                    assert!(!entry.red1());
669                    assert!(!entry.grn1());
670                    assert!(!entry.blu1());
671                    assert!(!entry.red2());
672                    assert!(!entry.grn2());
673                    assert!(!entry.blu2());
674                }
675            }
676        }
677    }
678
679    #[test]
680    fn entry_debug_shows_hex_value() {
681        let mut entry = Entry::new();
682        entry.set_red1(true);
683        let s = format!("{entry:?}");
684        assert!(s.contains("Entry"));
685        assert!(s.contains("0x"));
686    }
687
688    #[test]
689    fn entry_new_oe_matches_feature() {
690        let entry = Entry::new();
691        // `Entry::new()` starts blanked, so its OE bit reflects OE_BLANK.
692        assert_eq!(entry.output_enable(), !cfg!(feature = "invert-oe"));
693    }
694
695    #[test]
696    fn make_data_template_oe_polarity() {
697        let mut row = Row::<TEST_COLS>::new();
698        row.format(4);
699
700        let oe_active = cfg!(feature = "invert-oe");
701        let active_idx = map_index(TRAIL_BLANK_DELAY);
702        let blank_idx = map_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
703        let latch_idx = map_index(TEST_COLS - 1);
704
705        assert_eq!(row.data[active_idx].output_enable(), oe_active);
706        assert_eq!(row.data[blank_idx].output_enable(), !oe_active);
707        assert_eq!(row.data[latch_idx].output_enable(), !oe_active);
708    }
709
710    static STATIC_FB: TestBuffer = TestBuffer::new();
711
712    #[test]
713    fn test_static_construction_is_formatted() {
714        let runtime_fb = TestBuffer::new();
715
716        for (pi, plane) in STATIC_FB.planes.iter().enumerate() {
717            for (ri, row) in plane.rows.iter().enumerate() {
718                assert_eq!(
719                    row.data, runtime_fb.planes[pi].rows[ri].data,
720                    "static vs runtime mismatch at plane {pi}, row {ri}"
721                );
722                assert_eq!(
723                    row.gap, runtime_fb.planes[pi].rows[ri].gap,
724                    "static vs runtime gap mismatch at plane {pi}, row {ri}"
725                );
726            }
727        }
728    }
729
730    #[test]
731    fn test_format_reinitializes_at_runtime() {
732        let mut fb = TestBuffer::new();
733        fb.erase();
734        fb.format();
735
736        for (pi, plane) in fb.planes.iter().enumerate() {
737            for (ri, row) in plane.rows.iter().enumerate() {
738                assert_eq!(
739                    row.data, STATIC_FB.planes[pi].rows[ri].data,
740                    "re-formatted vs static mismatch at plane {pi}, row {ri}"
741                );
742            }
743        }
744    }
745
746    #[test]
747    fn bcm_segment_count_equals_planes() {
748        let fb = TestBuffer::new();
749        assert_eq!(fb.bcm_segment_count(), 8);
750    }
751
752    #[test]
753    fn bcm_segments_per_group_is_one() {
754        use crate::FrameBuffer;
755        let fb = TestBuffer::new();
756        assert_eq!(fb.bcm_segments_per_group(), 1);
757    }
758
759    #[test]
760    fn bcm_segments_have_correct_reps() {
761        use crate::FrameBuffer;
762        let fb = TestBuffer::new();
763        for i in 0..8 {
764            let seg = fb.bcm_segment(i);
765            let expected = if i == 0 { 1 } else { 1 << (i - 1) };
766            assert_eq!(seg.reps, expected, "wrong reps for segment {i}");
767        }
768    }
769
770    #[test]
771    fn bcm_segments_point_to_plane_data() {
772        use crate::FrameBuffer;
773        let fb = TestBuffer::new();
774        let plane_len = core::mem::size_of::<PlaneData<16, TEST_COLS>>();
775        for i in 0..8 {
776            let seg = fb.bcm_segment(i);
777            let expected_ptr = (&raw const fb.planes[i]).cast::<u8>();
778            assert_eq!(seg.ptr, expected_ptr, "wrong ptr for segment {i}");
779            assert_eq!(seg.len, plane_len * (8 - i), "wrong len for segment {i}");
780        }
781    }
782
783    #[test]
784    fn bcm_segment_total_reps_is_halved_by_coalescing() {
785        use crate::FrameBuffer;
786        let fb = TestBuffer::new();
787        let total: usize = (0..fb.bcm_segment_count())
788            .map(|i| fb.bcm_segment(i).reps)
789            .sum();
790        assert_eq!(total, 1 << (8 - 1));
791    }
792
793    #[test]
794    fn bcm_segment_plane_coverage_matches_bcm_weights() {
795        use crate::FrameBuffer;
796        let fb = TestBuffer::new();
797        let plane_bytes = core::mem::size_of::<PlaneData<16, TEST_COLS>>();
798
799        for plane in 0..8usize {
800            // Sum the reps of every segment that spans this plane: the
801            // segment for plane `first` covers `len / plane_bytes`
802            // consecutive planes starting at `first`.
803            let mut coverage = 0;
804            for first in 0..=plane {
805                let seg = fb.bcm_segment(first);
806                if first + seg.len / plane_bytes > plane {
807                    coverage += seg.reps;
808                }
809            }
810            assert_eq!(coverage, 1 << plane, "plane {plane} coverage wrong");
811        }
812    }
813
814    #[test]
815    #[should_panic(expected = "out of range")]
816    fn bcm_segment_panics_for_invalid_index() {
817        use crate::FrameBuffer;
818        let fb = TestBuffer::new();
819        let _ = fb.bcm_segment(8);
820    }
821
822    #[test]
823    fn bcm_sequence_matches_runtime_segments() {
824        use crate::FrameBuffer;
825        let fb = TestBuffer::new();
826        assert_eq!(
827            TestBuffer::BCM_SEGMENT_COUNT,
828            TestBuffer::BCM_SEQUENCE_LEN * TestBuffer::BCM_SEQUENCE_COUNT
829        );
830        assert_eq!(fb.bcm_segment_count(), TestBuffer::BCM_SEGMENT_COUNT);
831        assert_eq!(
832            fb.bcm_segments_per_group(),
833            TestBuffer::BCM_SEGMENTS_PER_GROUP
834        );
835        assert_eq!(
836            TestBuffer::BCM_SEQUENCE_LEN % TestBuffer::BCM_SEGMENTS_PER_GROUP,
837            0
838        );
839        for i in 0..TestBuffer::BCM_SEGMENT_COUNT {
840            let entry = TestBuffer::BCM_SEQUENCE[i % TestBuffer::BCM_SEQUENCE_LEN];
841            let seg = fb.bcm_segment(i);
842            assert_eq!((seg.len, seg.reps), (entry.len, entry.reps), "segment {i}");
843            assert!(!seg.ptr.is_null(), "segment {i} has null pointer");
844        }
845        for entry in &TestBuffer::BCM_SEQUENCE[TestBuffer::BCM_SEQUENCE_LEN..] {
846            assert_eq!(*entry, crate::BcmLenReps::ZERO, "padding must be zero");
847        }
848    }
849}