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