Skip to main content

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