hub75_framebuffer/
tiling.rs

1//! For tiling multiple displays together in various grid arrangements
2//! They have to be tiles together in some specific supported grid layouts.
3//! Currently supported layouts:
4//! - [`ChainTopRightDown`]
5//!
6//! To write to those panels the [`TiledFrameBuffer`] can be used.
7//! A usage example can be found at that structs documentation.
8
9use core::{convert::Infallible, marker::PhantomData};
10
11use crate::{Color, FrameBuffer, FrameBufferOperations, WordSize};
12#[cfg(not(feature = "esp-hal-dma"))]
13use embedded_dma::ReadBuffer;
14use embedded_graphics::prelude::{DrawTarget, OriginDimensions, PixelColor, Point, Size};
15#[cfg(feature = "esp-hal-dma")]
16use esp_hal::dma::ReadBuffer;
17
18/// Computes the number of columns needed if the displays are bing tiled together.
19/// # Arguments
20///
21/// * `cols` - Number of columns per panel
22/// * `num_panels_wide` - Number of panels tiled horizontally
23/// * `num_panels_high` - Number of panels tiled vertically
24///
25/// # Returns
26///
27/// Number of columns needed internally for `DmaFrameBuffer`
28#[must_use]
29pub const fn compute_tiled_cols(
30    cols: usize,
31    num_panels_wide: usize,
32    num_panels_high: usize,
33) -> usize {
34    cols * num_panels_wide * num_panels_high
35}
36
37/// Trait for pixel re-mappers
38///
39/// Implementors of this trait will remap x,y coordinates from a
40/// virtual panel to the actual framebuffer used to drive the panels
41///
42/// # Type Parameters
43///
44/// * `PANEL_ROWS` - Number of rows in a single panel
45/// * `PANEL_COLS` - Number of columns in a single panel
46/// * `TILE_ROWS` - Number of panels stacked vertically
47/// * `TILE_COLS` - Number of panels stacked horizontally
48pub trait PixelRemapper {
49    /// Number of rows in the virtual panel
50    const VIRT_ROWS: usize;
51    /// Number of columns in the virtual panel
52    const VIRT_COLS: usize;
53    /// Number of rows in the actual framebuffer
54    const FB_ROWS: usize;
55    /// Number of columns in the actual framebuffer
56    const FB_COLS: usize;
57
58    /// Remap a virtual pixel to a framebuffer pixel
59    #[inline]
60    fn remap<C: PixelColor>(mut pixel: embedded_graphics::Pixel<C>) -> embedded_graphics::Pixel<C> {
61        pixel.0 = Self::remap_point(pixel.0);
62        pixel
63    }
64
65    /// Remap a virtual point to a framebuffer point
66    #[inline]
67    #[must_use]
68    fn remap_point(mut point: Point) -> Point {
69        if point.x < 0 || point.y < 0 {
70            // Skip remapping points which are off the screen
71            return point;
72        }
73        let (re_x, re_y) = Self::remap_xy(point.x as usize, point.y as usize);
74        // If larger than u16, it is fair to assume that the point will be off the screen
75        point.x = i32::from(re_x as u16);
76        point.y = i32::from(re_y as u16);
77        point
78    }
79
80    /// Remap an x,y coordinate to a framebuffer pixel
81    fn remap_xy(x: usize, y: usize) -> (usize, usize);
82
83    /// Size of the virtual panel
84    #[inline]
85    #[must_use]
86    fn virtual_size() -> (usize, usize) {
87        (Self::VIRT_ROWS, Self::VIRT_COLS)
88    }
89
90    /// Size of the framebuffer that this remaps to
91    #[inline]
92    #[must_use]
93    fn fb_size() -> (usize, usize) {
94        (Self::FB_ROWS, Self::FB_COLS)
95    }
96}
97
98/// Chaining strategy for tiled panels
99///
100/// This type should be provided to the [`TiledFrameBuffer`] as a type argument.
101/// Take a look at its documentation for more details
102///
103/// When looking at the front, panels are chained together starting at the top right, chaining to the
104/// left until the end of the column. Then wrapping down to the next row where panels are chained left to right.
105/// This makes every second rows panels installed upside down.
106/// This pattern repeats until all rows of panels are covered.
107///
108/// # Type Parameters
109///
110/// * `PANEL_ROWS` - Number of rows in a single panel
111/// * `PANEL_COLS` - Number of columns in a single panel
112/// * `TILE_ROWS` - Number of panels stacked vertically
113/// * `TILE_COLS` - Number of panels stacked horizontally
114#[cfg_attr(feature = "defmt", derive(defmt::Format))]
115#[derive(core::fmt::Debug)]
116pub struct ChainTopRightDown<
117    const PANEL_ROWS: usize,
118    const PANEL_COLS: usize,
119    const TILE_ROWS: usize,
120    const TILE_COLS: usize,
121> {}
122
123impl<
124        const PANEL_ROWS: usize,
125        const PANEL_COLS: usize,
126        const TILE_ROWS: usize,
127        const TILE_COLS: usize,
128    > PixelRemapper for ChainTopRightDown<PANEL_ROWS, PANEL_COLS, TILE_ROWS, TILE_COLS>
129{
130    const VIRT_ROWS: usize = PANEL_ROWS * TILE_ROWS;
131    const VIRT_COLS: usize = PANEL_COLS * TILE_COLS;
132    const FB_ROWS: usize = PANEL_ROWS;
133    const FB_COLS: usize = PANEL_COLS * TILE_ROWS * TILE_COLS;
134
135    fn remap_xy(x: usize, y: usize) -> (usize, usize) {
136        // 0 = top row, 1 = next row, …
137        let row = y / PANEL_ROWS;
138        let base = (TILE_ROWS - 1 - row) * Self::VIRT_COLS;
139
140        if row % 2 == 1 {
141            // this row is mounted upside-down
142            (
143                base + Self::VIRT_COLS - 1 - x, // mirror x across the whole virtual row
144                PANEL_ROWS - 1 - (y % PANEL_ROWS), // flip y within the panel
145            )
146        } else {
147            (base + x, y % PANEL_ROWS) // normal orientation
148        }
149    }
150}
151
152/// Tile together multiple displays in a certain configuration to form a single larger display
153///
154/// This is a wrapper around an actual framebuffer implementation which can be used to tile multiple
155/// LED matrices together by using a certain pixel remapping strategy.
156///
157/// # Type Parameters
158/// - `F` - The type of the underlying framebuffer which will drive the display
159/// - `M` - The pixel remapping strategy (see implementers of [`PixelRemapper`]) to use to map the virtual framebuffer to the actual framebuffer
160/// - `PANEL_ROWS` - Number of rows in a single panel
161/// - `PANEL_COLS` - Number of columns in a single panel
162/// - `NROWS`: Number of rows per scan (typically half of ROWS)
163/// - `BITS`: Color depth (1-8 bits)
164/// - `FRAME_COUNT`: Number of frames used for Binary Code Modulation
165/// * `TILE_ROWS` - Number of panels stacked vertically
166/// * `TILE_COLS` - Number of panels stacked horizontally
167/// * `FB_COLS` - Number of columns that the actual framebuffer must have to drive all display
168///
169/// # Example
170/// ```rust
171/// use hub75_framebuffer::{compute_frame_count, compute_rows};
172/// use hub75_framebuffer::plain::DmaFrameBuffer;
173/// use hub75_framebuffer::tiling::{TiledFrameBuffer, ChainTopRightDown, compute_tiled_cols};
174///
175/// const TILED_COLS: usize = 3;
176/// const TILED_ROWS: usize = 3;
177/// const ROWS: usize = 32;
178/// const PANEL_COLS: usize = 64;
179/// const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
180/// const BITS: u8 = 2;
181/// const NROWS: usize = compute_rows(ROWS);
182/// const FRAME_COUNT: usize = compute_frame_count(BITS);
183///
184/// type FBType = DmaFrameBuffer<ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>;
185/// type TiledFBType = TiledFrameBuffer<
186///     FBType,
187///     ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
188///     ROWS,
189///     PANEL_COLS,
190///     NROWS,
191///     BITS,
192///     FRAME_COUNT,
193///     TILED_ROWS,
194///     TILED_COLS,
195///     FB_COLS,
196/// >;
197///
198/// let mut fb = TiledFBType::new();
199///
200/// // Now fb is ready to be used and can be treated like one big canvas (192*96 pixels in this example)
201/// ```
202#[cfg_attr(feature = "defmt", derive(defmt::Format))]
203#[derive(core::fmt::Debug)]
204pub struct TiledFrameBuffer<
205    F,
206    M: PixelRemapper,
207    const PANEL_ROWS: usize,
208    const PANEL_COLS: usize,
209    const NROWS: usize,
210    const BITS: u8,
211    const FRAME_COUNT: usize,
212    const TILE_ROWS: usize,
213    const TILE_COLS: usize,
214    const FB_COLS: usize,
215>(F, PhantomData<M>);
216
217impl<
218        F: Default,
219        M: PixelRemapper,
220        const PANEL_ROWS: usize,
221        const PANEL_COLS: usize,
222        const NROWS: usize,
223        const BITS: u8,
224        const FRAME_COUNT: usize,
225        const TILE_ROWS: usize,
226        const TILE_COLS: usize,
227        const FB_COLS: usize,
228    >
229    TiledFrameBuffer<
230        F,
231        M,
232        PANEL_ROWS,
233        PANEL_COLS,
234        NROWS,
235        BITS,
236        FRAME_COUNT,
237        TILE_ROWS,
238        TILE_COLS,
239        FB_COLS,
240    >
241{
242    /// Create a new "virtual display" that takes ownership of the underlying framebuffer
243    /// and remaps any pixels written to it to the correct locations of the underlying framebuffer
244    /// based on the given `PixelRemapper`
245    #[must_use]
246    pub fn new() -> Self {
247        Self(F::default(), PhantomData)
248    }
249}
250
251impl<
252        F: Default,
253        M: PixelRemapper,
254        const PANEL_ROWS: usize,
255        const PANEL_COLS: usize,
256        const NROWS: usize,
257        const BITS: u8,
258        const FRAME_COUNT: usize,
259        const TILE_ROWS: usize,
260        const TILE_COLS: usize,
261        const FB_COLS: usize,
262    > Default
263    for TiledFrameBuffer<
264        F,
265        M,
266        PANEL_ROWS,
267        PANEL_COLS,
268        NROWS,
269        BITS,
270        FRAME_COUNT,
271        TILE_ROWS,
272        TILE_COLS,
273        FB_COLS,
274    >
275{
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281impl<
282        F: DrawTarget<Error = Infallible, Color = Color>,
283        M: PixelRemapper,
284        const PANEL_ROWS: usize,
285        const PANEL_COLS: usize,
286        const NROWS: usize,
287        const BITS: u8,
288        const FRAME_COUNT: usize,
289        const TILE_ROWS: usize,
290        const TILE_COLS: usize,
291        const FB_COLS: usize,
292    > DrawTarget
293    for TiledFrameBuffer<
294        F,
295        M,
296        PANEL_ROWS,
297        PANEL_COLS,
298        NROWS,
299        BITS,
300        FRAME_COUNT,
301        TILE_ROWS,
302        TILE_COLS,
303        FB_COLS,
304    >
305{
306    type Color = Color;
307    type Error = Infallible;
308
309    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
310    where
311        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
312    {
313        self.0.draw_iter(pixels.into_iter().map(M::remap))
314    }
315}
316
317impl<
318        F: DrawTarget<Error = Infallible, Color = Color>,
319        M: PixelRemapper,
320        const PANEL_ROWS: usize,
321        const PANEL_COLS: usize,
322        const NROWS: usize,
323        const BITS: u8,
324        const FRAME_COUNT: usize,
325        const TILE_ROWS: usize,
326        const TILE_COLS: usize,
327        const FB_COLS: usize,
328    > OriginDimensions
329    for TiledFrameBuffer<
330        F,
331        M,
332        PANEL_ROWS,
333        PANEL_COLS,
334        NROWS,
335        BITS,
336        FRAME_COUNT,
337        TILE_ROWS,
338        TILE_COLS,
339        FB_COLS,
340    >
341{
342    fn size(&self) -> Size {
343        Size::new(M::virtual_size().1 as u32, M::virtual_size().0 as u32)
344    }
345}
346
347impl<
348        F: FrameBufferOperations<PANEL_ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>
349            + FrameBuffer<PANEL_ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>,
350        M: PixelRemapper,
351        const PANEL_ROWS: usize,
352        const PANEL_COLS: usize,
353        const NROWS: usize,
354        const BITS: u8,
355        const FRAME_COUNT: usize,
356        const TILE_ROWS: usize,
357        const TILE_COLS: usize,
358        const FB_COLS: usize,
359    > FrameBufferOperations<PANEL_ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>
360    for TiledFrameBuffer<
361        F,
362        M,
363        PANEL_ROWS,
364        PANEL_COLS,
365        NROWS,
366        BITS,
367        FRAME_COUNT,
368        TILE_ROWS,
369        TILE_COLS,
370        FB_COLS,
371    >
372{
373    #[inline]
374    fn erase(&mut self) {
375        self.0.erase();
376    }
377
378    #[inline]
379    fn set_pixel(&mut self, p: Point, color: Color) {
380        self.0.set_pixel(M::remap_point(p), color);
381    }
382}
383
384#[cfg(not(feature = "esp-hal-dma"))]
385unsafe impl<
386        T,
387        F: ReadBuffer<Word = T>,
388        M: PixelRemapper,
389        const PANEL_ROWS: usize,
390        const PANEL_COLS: usize,
391        const NROWS: usize,
392        const BITS: u8,
393        const FRAME_COUNT: usize,
394        const TILE_ROWS: usize,
395        const TILE_COLS: usize,
396        const FB_COLS: usize,
397    > ReadBuffer
398    for TiledFrameBuffer<
399        F,
400        M,
401        PANEL_ROWS,
402        PANEL_COLS,
403        NROWS,
404        BITS,
405        FRAME_COUNT,
406        TILE_ROWS,
407        TILE_COLS,
408        FB_COLS,
409    >
410{
411    type Word = T;
412
413    unsafe fn read_buffer(&self) -> (*const T, usize) {
414        self.0.read_buffer()
415    }
416}
417
418#[cfg(feature = "esp-hal-dma")]
419unsafe impl<
420        F: ReadBuffer,
421        M: PixelRemapper,
422        const PANEL_ROWS: usize,
423        const PANEL_COLS: usize,
424        const NROWS: usize,
425        const BITS: u8,
426        const FRAME_COUNT: usize,
427        const TILE_ROWS: usize,
428        const TILE_COLS: usize,
429        const FB_COLS: usize,
430    > ReadBuffer
431    for TiledFrameBuffer<
432        F,
433        M,
434        PANEL_ROWS,
435        PANEL_COLS,
436        NROWS,
437        BITS,
438        FRAME_COUNT,
439        TILE_ROWS,
440        TILE_COLS,
441        FB_COLS,
442    >
443{
444    unsafe fn read_buffer(&self) -> (*const u8, usize) {
445        self.0.read_buffer()
446    }
447}
448
449impl<
450        F: FrameBuffer<PANEL_ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>,
451        M: PixelRemapper,
452        const PANEL_ROWS: usize,
453        const PANEL_COLS: usize,
454        const NROWS: usize,
455        const BITS: u8,
456        const FRAME_COUNT: usize,
457        const TILE_ROWS: usize,
458        const TILE_COLS: usize,
459        const FB_COLS: usize,
460    > FrameBuffer<PANEL_ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>
461    for TiledFrameBuffer<
462        F,
463        M,
464        PANEL_ROWS,
465        PANEL_COLS,
466        NROWS,
467        BITS,
468        FRAME_COUNT,
469        TILE_ROWS,
470        TILE_COLS,
471        FB_COLS,
472    >
473{
474    fn get_word_size(&self) -> WordSize {
475        self.0.get_word_size()
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    extern crate std;
482
483    use embedded_graphics::prelude::*;
484
485    use super::*;
486    use core::convert::Infallible;
487
488    #[test]
489    fn test_virtual_size_function_with_equal_rows_and_cols() {
490        const ROWS_IN_PANEL: usize = 32;
491        const COLS_IN_PANEL: usize = 64;
492        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 3>;
493        let virt_size = PanelChain::virtual_size();
494        assert_eq!(virt_size, (ROWS_IN_PANEL * 3, COLS_IN_PANEL * 3));
495    }
496
497    #[test]
498    fn test_virtual_size_function_with_uneven_rows_and_cols() {
499        const ROWS_IN_PANEL: usize = 32;
500        const COLS_IN_PANEL: usize = 64;
501        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 5, 3>;
502        let virt_size = PanelChain::virtual_size();
503        assert_eq!(virt_size, (ROWS_IN_PANEL * 5, COLS_IN_PANEL * 3));
504    }
505
506    #[test]
507    fn test_virtual_size_function_with_single_column() {
508        const ROWS_IN_PANEL: usize = 32;
509        const COLS_IN_PANEL: usize = 64;
510        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 1>;
511        let virt_size = PanelChain::virtual_size();
512        assert_eq!(virt_size, (ROWS_IN_PANEL * 3, COLS_IN_PANEL));
513    }
514
515    #[test]
516    fn test_fb_size_function_with_equal_rows_and_cols() {
517        const ROWS_IN_PANEL: usize = 32;
518        const COLS_IN_PANEL: usize = 64;
519        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 3>;
520        let virt_size = PanelChain::fb_size();
521        assert_eq!(virt_size, (ROWS_IN_PANEL, COLS_IN_PANEL * 9));
522    }
523
524    #[test]
525    fn test_fb_size_function_with_uneven_rows_and_cols() {
526        const ROWS_IN_PANEL: usize = 32;
527        const COLS_IN_PANEL: usize = 64;
528        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 5, 3>;
529        let virt_size = PanelChain::fb_size();
530        assert_eq!(virt_size, (ROWS_IN_PANEL, COLS_IN_PANEL * 15));
531    }
532
533    #[test]
534    fn test_fb_size_function_with_single_column() {
535        const ROWS_IN_PANEL: usize = 32;
536        const COLS_IN_PANEL: usize = 64;
537        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 1>;
538        let virt_size = PanelChain::fb_size();
539        assert_eq!(virt_size, (ROWS_IN_PANEL, COLS_IN_PANEL * 3));
540    }
541
542    #[test]
543    fn test_pixel_remap_top_right_down_point_in_origin() {
544        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
545
546        let pixel = PanelChain::remap(Pixel(Point::new(0, 0), Color::RED));
547        assert_eq!(pixel.0, Point::new(384, 0));
548    }
549
550    #[test]
551    fn test_pixel_remap_top_right_down_point_in_bottom_left_corner() {
552        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
553
554        let pixel = PanelChain::remap(Pixel(Point::new(0, 95), Color::RED));
555        assert_eq!(pixel.0, Point::new(0, 31));
556    }
557
558    #[test]
559    fn test_pixel_remap_top_right_down_point_in_bottom_right_corner() {
560        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
561
562        let pixel = PanelChain::remap(Pixel(Point::new(191, 95), Color::RED));
563        assert_eq!(pixel.0, Point::new(191, 31));
564    }
565
566    #[test]
567    fn test_pixel_remap_top_right_down_point_on_x_right_edge_of_first_panel() {
568        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
569
570        let pixel = PanelChain::remap(Pixel(Point::new(63, 0), Color::RED));
571        assert_eq!(pixel.0, Point::new(447, 0));
572    }
573
574    #[test]
575    fn test_pixel_remap_top_right_down_point_on_x_left_edge_of_second_panel() {
576        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
577
578        let pixel = PanelChain::remap(Pixel(Point::new(64, 0), Color::RED));
579        assert_eq!(pixel.0, Point::new(448, 0));
580    }
581
582    #[test]
583    fn test_pixel_remap_top_right_down_point_on_y_bottom_edge_of_first_panel() {
584        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
585
586        let pixel = PanelChain::remap(Pixel(Point::new(0, 31), Color::RED));
587        assert_eq!(pixel.0, Point::new(384, 31));
588    }
589
590    #[test]
591    fn test_pixel_remap_top_right_down_point_on_y_top_edge_of_fourth_panel() {
592        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
593
594        let pixel = PanelChain::remap(Pixel(Point::new(0, 32), Color::RED));
595        assert_eq!(pixel.0, Point::new(383, 31));
596    }
597
598    #[test]
599    fn test_pixel_remap_top_right_down_point_slightly_to_the_top_middle() {
600        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
601
602        let pixel = PanelChain::remap(Pixel(Point::new(100, 40), Color::RED));
603        assert_eq!(pixel.0, Point::new(283, 23));
604    }
605
606    #[test]
607    fn test_pixel_remap_negative_pixel_does_not_remap() {
608        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
609
610        let pixel = PanelChain::remap(Pixel(Point::new(-5, 40), Color::RED));
611        assert_eq!(pixel.0, Point::new(-5, 40));
612    }
613
614    #[test]
615    fn test_compute_tiled_cols() {
616        assert_eq!(192, compute_tiled_cols(32, 3, 2));
617    }
618
619    #[test]
620    fn test_tiling_framebuffer_canvas_size() {
621        use crate::plain::DmaFrameBuffer;
622        use crate::tiling::{compute_tiled_cols, ChainTopRightDown, TiledFrameBuffer};
623        use crate::{compute_frame_count, compute_rows};
624
625        const TILED_COLS: usize = 3;
626        const TILED_ROWS: usize = 3;
627        const ROWS: usize = 32;
628        const PANEL_COLS: usize = 64;
629        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
630        const BITS: u8 = 2;
631        const NROWS: usize = compute_rows(ROWS);
632        const FRAME_COUNT: usize = compute_frame_count(BITS);
633
634        type FBType = DmaFrameBuffer<ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>;
635        type TiledFBType = TiledFrameBuffer<
636            FBType,
637            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
638            ROWS,
639            PANEL_COLS,
640            NROWS,
641            BITS,
642            FRAME_COUNT,
643            TILED_ROWS,
644            TILED_COLS,
645            FB_COLS,
646        >;
647
648        let fb = TiledFBType::new();
649
650        assert_eq!(fb.size(), Size::new(192, 96));
651    }
652
653    // Test helper framebuffer that records calls for verification
654    struct TestFrameBuffer {
655        calls: std::cell::RefCell<std::vec::Vec<Call>>,
656        buf: [u8; 8],
657        word_size: WordSize,
658    }
659
660    #[derive(Debug, Clone, PartialEq, Eq)]
661    enum Call {
662        Erase,
663        SetPixel { p: Point, color: Color },
664        Draw(std::vec::Vec<(Point, Color)>),
665    }
666
667    impl TestFrameBuffer {
668        fn new(word_size: WordSize) -> Self {
669            Self {
670                calls: std::cell::RefCell::new(std::vec::Vec::new()),
671                buf: [0; 8],
672                word_size,
673            }
674        }
675
676        fn take_calls(&self) -> std::vec::Vec<Call> {
677            core::mem::take(&mut *self.calls.borrow_mut())
678        }
679    }
680
681    impl Default for TestFrameBuffer {
682        fn default() -> Self {
683            Self::new(WordSize::Eight)
684        }
685    }
686
687    impl DrawTarget for TestFrameBuffer {
688        type Color = Color;
689        type Error = Infallible;
690
691        fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
692        where
693            I: IntoIterator<Item = Pixel<Self::Color>>,
694        {
695            let v = pixels.into_iter().map(|p| (p.0, p.1)).collect();
696            self.calls.borrow_mut().push(Call::Draw(v));
697            Ok(())
698        }
699    }
700
701    impl OriginDimensions for TestFrameBuffer {
702        fn size(&self) -> Size {
703            Size::new(1, 1)
704        }
705    }
706
707    impl<
708            const ROWS: usize,
709            const COLS: usize,
710            const NROWS: usize,
711            const BITS: u8,
712            const FRAME_COUNT: usize,
713        > FrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT> for TestFrameBuffer
714    {
715        fn get_word_size(&self) -> WordSize {
716            self.word_size
717        }
718    }
719
720    impl<
721            const ROWS: usize,
722            const COLS: usize,
723            const NROWS: usize,
724            const BITS: u8,
725            const FRAME_COUNT: usize,
726        > FrameBufferOperations<ROWS, COLS, NROWS, BITS, FRAME_COUNT> for TestFrameBuffer
727    {
728        fn erase(&mut self) {
729            self.calls.borrow_mut().push(Call::Erase);
730        }
731
732        fn set_pixel(&mut self, p: Point, color: Color) {
733            self.calls.borrow_mut().push(Call::SetPixel { p, color });
734        }
735    }
736
737    #[cfg(not(feature = "esp-hal-dma"))]
738    unsafe impl embedded_dma::ReadBuffer for TestFrameBuffer {
739        type Word = u8;
740
741        unsafe fn read_buffer(&self) -> (*const u8, usize) {
742            (self.buf.as_ptr(), self.buf.len())
743        }
744    }
745
746    #[cfg(feature = "esp-hal-dma")]
747    unsafe impl esp_hal::dma::ReadBuffer for TestFrameBuffer {
748        unsafe fn read_buffer(&self) -> (*const u8, usize) {
749            (self.buf.as_ptr(), self.buf.len())
750        }
751    }
752
753    #[test]
754    fn test_tiled_draw_iter_forwards_with_remap() {
755        const TILED_COLS: usize = 3;
756        const TILED_ROWS: usize = 3;
757        const ROWS: usize = 32;
758        const PANEL_COLS: usize = 64;
759        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
760
761        let mut fb = TiledFrameBuffer::<
762            TestFrameBuffer,
763            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
764            ROWS,
765            PANEL_COLS,
766            { crate::compute_rows(ROWS) },
767            2,
768            { crate::compute_frame_count(2) },
769            TILED_ROWS,
770            TILED_COLS,
771            FB_COLS,
772        >(
773            TestFrameBuffer::new(WordSize::Eight),
774            core::marker::PhantomData,
775        );
776
777        let input = [
778            Pixel(Point::new(0, 0), Color::RED),
779            Pixel(Point::new(63, 0), Color::GREEN),
780            Pixel(Point::new(64, 0), Color::BLUE),
781            Pixel(Point::new(100, 40), Color::WHITE),
782        ];
783
784        fb.draw_iter(input.into_iter()).unwrap();
785
786        let calls = fb.0.take_calls();
787        assert_eq!(calls.len(), 1);
788        match &calls[0] {
789            Call::Draw(v) => {
790                let expected =
791                    [
792                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
793                            Pixel(Point::new(0, 0), Color::RED),
794                        ),
795                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
796                            Pixel(Point::new(63, 0), Color::GREEN),
797                        ),
798                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
799                            Pixel(Point::new(64, 0), Color::BLUE),
800                        ),
801                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
802                            Pixel(Point::new(100, 40), Color::WHITE),
803                        ),
804                    ];
805                let expected_points: std::vec::Vec<(Point, Color)> =
806                    expected.iter().map(|p| (p.0, p.1)).collect();
807                assert_eq!(v.as_slice(), expected_points.as_slice());
808            }
809            _ => panic!("expected a Draw call"),
810        }
811    }
812
813    #[test]
814    fn test_tiled_set_pixel_remaps_and_forwards() {
815        const TILED_COLS: usize = 3;
816        const TILED_ROWS: usize = 3;
817        const ROWS: usize = 32;
818        const PANEL_COLS: usize = 64;
819        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
820
821        let mut fb = TiledFrameBuffer::<
822            TestFrameBuffer,
823            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
824            ROWS,
825            PANEL_COLS,
826            { crate::compute_rows(ROWS) },
827            2,
828            { crate::compute_frame_count(2) },
829            TILED_ROWS,
830            TILED_COLS,
831            FB_COLS,
832        >(
833            TestFrameBuffer::new(WordSize::Eight),
834            core::marker::PhantomData,
835        );
836
837        let p = Point::new(100, 40);
838        fb.set_pixel(p, Color::BLUE);
839
840        let calls = fb.0.take_calls();
841        assert_eq!(calls.len(), 1);
842        match calls.into_iter().next().unwrap() {
843            Call::SetPixel { p: rp, color } => {
844                let expected =
845                    ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap_point(p);
846                assert_eq!(rp, expected);
847                assert_eq!(color, Color::BLUE);
848            }
849            _ => panic!("expected a SetPixel call"),
850        }
851    }
852
853    #[test]
854    fn test_tiled_erase_forwards() {
855        const TILED_COLS: usize = 2;
856        const TILED_ROWS: usize = 2;
857        const ROWS: usize = 32;
858        const PANEL_COLS: usize = 64;
859        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
860
861        let mut fb = TiledFrameBuffer::<
862            TestFrameBuffer,
863            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
864            ROWS,
865            PANEL_COLS,
866            { crate::compute_rows(ROWS) },
867            2,
868            { crate::compute_frame_count(2) },
869            TILED_ROWS,
870            TILED_COLS,
871            FB_COLS,
872        >(
873            TestFrameBuffer::new(WordSize::Eight),
874            core::marker::PhantomData,
875        );
876        fb.erase();
877        let calls = fb.0.take_calls();
878        assert_eq!(calls, std::vec![Call::Erase]);
879    }
880
881    #[test]
882    fn test_tiled_negative_coordinates_not_remapped() {
883        const TILED_COLS: usize = 2;
884        const TILED_ROWS: usize = 2;
885        const ROWS: usize = 32;
886        const PANEL_COLS: usize = 64;
887        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
888
889        let mut fb = TiledFrameBuffer::<
890            TestFrameBuffer,
891            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
892            ROWS,
893            PANEL_COLS,
894            { crate::compute_rows(ROWS) },
895            2,
896            { crate::compute_frame_count(2) },
897            TILED_ROWS,
898            TILED_COLS,
899            FB_COLS,
900        >(
901            TestFrameBuffer::new(WordSize::Eight),
902            core::marker::PhantomData,
903        );
904
905        // set_pixel path
906        let neg = Point::new(-3, 5);
907        fb.set_pixel(neg, Color::GREEN);
908        // draw_iter path
909        fb.draw_iter(core::iter::once(Pixel(Point::new(10, -2), Color::RED)))
910            .unwrap();
911
912        let calls = fb.0.take_calls();
913        assert_eq!(calls.len(), 2);
914        assert!(matches!(calls[0], Call::SetPixel { p, .. } if p == neg));
915        match &calls[1] {
916            Call::Draw(v) => {
917                assert_eq!(v.as_slice(), &[(Point::new(10, -2), Color::RED)]);
918            }
919            _ => panic!("expected a Draw call"),
920        }
921    }
922
923    #[test]
924    fn test_tiled_read_buffer_passthrough() {
925        const TILED_COLS: usize = 2;
926        const TILED_ROWS: usize = 2;
927        const ROWS: usize = 32;
928        const PANEL_COLS: usize = 64;
929        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
930
931        let fb = TiledFrameBuffer::<
932            TestFrameBuffer,
933            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
934            ROWS,
935            PANEL_COLS,
936            { crate::compute_rows(ROWS) },
937            2,
938            { crate::compute_frame_count(2) },
939            TILED_ROWS,
940            TILED_COLS,
941            FB_COLS,
942        >(
943            TestFrameBuffer::new(WordSize::Eight),
944            core::marker::PhantomData,
945        );
946
947        let inner_ptr = fb.0.buf.as_ptr();
948        let inner_len = fb.0.buf.len();
949
950        let (ptr, len) = unsafe { fb.read_buffer() };
951        assert_eq!(ptr, inner_ptr);
952        assert_eq!(len, inner_len);
953    }
954
955    #[test]
956    fn test_tiled_get_word_size_passthrough() {
957        const TILED_COLS: usize = 2;
958        const TILED_ROWS: usize = 2;
959        const ROWS: usize = 32;
960        const PANEL_COLS: usize = 64;
961        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
962
963        let fb = TiledFrameBuffer::<
964            TestFrameBuffer,
965            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
966            ROWS,
967            PANEL_COLS,
968            { crate::compute_rows(ROWS) },
969            2,
970            { crate::compute_frame_count(2) },
971            TILED_ROWS,
972            TILED_COLS,
973            FB_COLS,
974        >(
975            TestFrameBuffer::new(WordSize::Sixteen),
976            core::marker::PhantomData,
977        );
978        assert_eq!(fb.get_word_size(), WordSize::Sixteen);
979    }
980
981    #[test]
982    fn test_tiled_get_word_size_eight_passthrough() {
983        const TILED_COLS: usize = 2;
984        const TILED_ROWS: usize = 2;
985        const ROWS: usize = 32;
986        const PANEL_COLS: usize = 64;
987        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
988
989        let fb = TiledFrameBuffer::<
990            TestFrameBuffer,
991            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
992            ROWS,
993            PANEL_COLS,
994            { crate::compute_rows(ROWS) },
995            2,
996            { crate::compute_frame_count(2) },
997            TILED_ROWS,
998            TILED_COLS,
999            FB_COLS,
1000        >(
1001            TestFrameBuffer::new(WordSize::Eight),
1002            core::marker::PhantomData,
1003        );
1004        assert_eq!(fb.get_word_size(), WordSize::Eight);
1005    }
1006
1007    // Remapper that generates very large coordinates to trigger u16 truncation in remap_point
1008    struct Huge<
1009        const PANEL_ROWS: usize,
1010        const PANEL_COLS: usize,
1011        const TILE_ROWS: usize,
1012        const TILE_COLS: usize,
1013    >;
1014
1015    impl<
1016            const PANEL_ROWS: usize,
1017            const PANEL_COLS: usize,
1018            const TILE_ROWS: usize,
1019            const TILE_COLS: usize,
1020        > PixelRemapper for Huge<PANEL_ROWS, PANEL_COLS, TILE_ROWS, TILE_COLS>
1021    {
1022        const VIRT_ROWS: usize = PANEL_ROWS * TILE_ROWS;
1023        const VIRT_COLS: usize = PANEL_COLS * TILE_COLS;
1024        const FB_ROWS: usize = PANEL_ROWS;
1025        const FB_COLS: usize = PANEL_COLS * TILE_ROWS * TILE_COLS;
1026
1027        fn remap_xy(x: usize, y: usize) -> (usize, usize) {
1028            (x + 70_000, y + 70_000)
1029        }
1030    }
1031
1032    #[test]
1033    fn test_remap_point_truncates_to_u16_range() {
1034        const TILED_COLS: usize = 1;
1035        const TILED_ROWS: usize = 1;
1036        const ROWS: usize = 32;
1037        const PANEL_COLS: usize = 64;
1038        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1039
1040        let mut fb = TiledFrameBuffer::<
1041            TestFrameBuffer,
1042            Huge<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1043            ROWS,
1044            PANEL_COLS,
1045            { crate::compute_rows(ROWS) },
1046            2,
1047            { crate::compute_frame_count(2) },
1048            TILED_ROWS,
1049            TILED_COLS,
1050            FB_COLS,
1051        >(
1052            TestFrameBuffer::new(WordSize::Eight),
1053            core::marker::PhantomData,
1054        );
1055        fb.set_pixel(Point::new(1, 2), Color::RED);
1056
1057        let calls = fb.0.take_calls();
1058        match calls.into_iter().next().unwrap() {
1059            Call::SetPixel { p, color } => {
1060                let (rx, ry) = (1usize + 70_000, 2usize + 70_000);
1061                let expected = Point::new(i32::from(rx as u16), i32::from(ry as u16));
1062                assert_eq!(p, expected);
1063                assert_eq!(color, Color::RED);
1064            }
1065            other => panic!("unexpected call recorded: {other:?}"),
1066        }
1067    }
1068
1069    #[test]
1070    fn test_more_compute_tiled_cols_cases() {
1071        assert_eq!(compute_tiled_cols(64, 1, 4), 256);
1072        assert_eq!(compute_tiled_cols(64, 4, 1), 256);
1073        assert_eq!(compute_tiled_cols(32, 4, 5), 640);
1074    }
1075
1076    #[test]
1077    fn test_tiled_default_and_new_construct() {
1078        const TILED_COLS: usize = 4;
1079        const TILED_ROWS: usize = 2;
1080        const ROWS: usize = 32;
1081        const PANEL_COLS: usize = 64;
1082        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1083
1084        let fb_default = TiledFrameBuffer::<
1085            TestFrameBuffer,
1086            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1087            ROWS,
1088            PANEL_COLS,
1089            { crate::compute_rows(ROWS) },
1090            2,
1091            { crate::compute_frame_count(2) },
1092            TILED_ROWS,
1093            TILED_COLS,
1094            FB_COLS,
1095        >::default();
1096
1097        let fb_new = TiledFrameBuffer::<
1098            TestFrameBuffer,
1099            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1100            ROWS,
1101            PANEL_COLS,
1102            { crate::compute_rows(ROWS) },
1103            2,
1104            { crate::compute_frame_count(2) },
1105            TILED_ROWS,
1106            TILED_COLS,
1107            FB_COLS,
1108        >::new();
1109
1110        // Default constructs inner TestFrameBuffer::default() which uses WordSize::Eight
1111        assert_eq!(fb_default.get_word_size(), WordSize::Eight);
1112        assert_eq!(fb_new.get_word_size(), WordSize::Eight);
1113
1114        // Size comes from OriginDimensions impl on TiledFrameBuffer (via M::virtual_size)
1115        let expected_size = Size::new((PANEL_COLS * TILED_COLS) as u32, (ROWS * TILED_ROWS) as u32);
1116        assert_eq!(fb_default.size(), expected_size);
1117        assert_eq!(fb_new.size(), expected_size);
1118
1119        // No calls recorded yet on inner framebuffer
1120        assert!(fb_default.0.take_calls().is_empty());
1121        assert!(fb_new.0.take_calls().is_empty());
1122    }
1123
1124    #[test]
1125    fn test_tiled_origin_dimensions_matches_virtual_size() {
1126        const TILED_COLS: usize = 5;
1127        const TILED_ROWS: usize = 2;
1128        const ROWS: usize = 32;
1129        const PANEL_COLS: usize = 64;
1130        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1131
1132        let fb = TiledFrameBuffer::<
1133            TestFrameBuffer,
1134            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1135            ROWS,
1136            PANEL_COLS,
1137            { crate::compute_rows(ROWS) },
1138            2,
1139            { crate::compute_frame_count(2) },
1140            TILED_ROWS,
1141            TILED_COLS,
1142            FB_COLS,
1143        >::new();
1144
1145        let (virt_rows, virt_cols) = <ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS> as PixelRemapper>::virtual_size();
1146        assert_eq!(fb.size(), Size::new(virt_cols as u32, virt_rows as u32));
1147    }
1148
1149    // Expected mapping for ChainTopRightDown (current, correct behavior)
1150    fn expected_ctrdd_xy<const PR: usize, const PC: usize, const TR: usize, const TC: usize>(
1151        x: usize,
1152        y: usize,
1153    ) -> (usize, usize) {
1154        let row = y / PR;
1155        let base = (TR - 1 - row) * (PC * TC);
1156        if row % 2 == 1 {
1157            (base + (PC * TC) - 1 - x, PR - 1 - (y % PR))
1158        } else {
1159            (base + x, y % PR)
1160        }
1161    }
1162
1163    #[test]
1164    fn test_chain_top_right_down_corners_2x3() {
1165        const PR: usize = 32;
1166        const PC: usize = 64;
1167        const TR: usize = 2;
1168        const TC: usize = 3;
1169        type M = ChainTopRightDown<PR, PC, TR, TC>;
1170
1171        for r in 0..TR {
1172            for c in 0..TC {
1173                let x0 = c * PC;
1174                let y0 = r * PR;
1175                let corners = [
1176                    (x0, y0),                   // TL
1177                    (x0 + PC - 1, y0),          // TR
1178                    (x0, y0 + PR - 1),          // BL
1179                    (x0 + PC - 1, y0 + PR - 1), // BR
1180                ];
1181
1182                for &(x, y) in &corners {
1183                    let got = <M as PixelRemapper>::remap_xy(x, y);
1184                    let exp = expected_ctrdd_xy::<PR, PC, TR, TC>(x, y);
1185                    assert_eq!(
1186                        got, exp,
1187                        "corner mismatch at panel (row={}, col={}), virtual=({}, {})",
1188                        r, c, x, y
1189                    );
1190                }
1191            }
1192        }
1193    }
1194
1195    #[test]
1196    fn test_chain_top_right_down_corners_3x2() {
1197        const PR: usize = 32;
1198        const PC: usize = 64;
1199        const TR: usize = 3;
1200        const TC: usize = 2;
1201        type M = ChainTopRightDown<PR, PC, TR, TC>;
1202
1203        for r in 0..TR {
1204            for c in 0..TC {
1205                let x0 = c * PC;
1206                let y0 = r * PR;
1207                let corners = [
1208                    (x0, y0),                   // TL
1209                    (x0 + PC - 1, y0),          // TR
1210                    (x0, y0 + PR - 1),          // BL
1211                    (x0 + PC - 1, y0 + PR - 1), // BR
1212                ];
1213
1214                for &(x, y) in &corners {
1215                    let got = <M as PixelRemapper>::remap_xy(x, y);
1216                    let exp = expected_ctrdd_xy::<PR, PC, TR, TC>(x, y);
1217                    assert_eq!(
1218                        got, exp,
1219                        "corner mismatch at panel (row={}, col={}), virtual=({}, {})",
1220                        r, c, x, y
1221                    );
1222                }
1223            }
1224        }
1225    }
1226}