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    type Word = F::Word;
439
440    fn get_word_size(&self) -> WordSize {
441        self.0.get_word_size()
442    }
443
444    fn plane_count(&self) -> usize {
445        self.0.plane_count()
446    }
447
448    fn plane_ptr_len(&self, plane_idx: usize) -> (*const u8, usize) {
449        self.0.plane_ptr_len(plane_idx)
450    }
451}
452
453impl<
454        F: MutableFrameBuffer,
455        M: PixelRemapper,
456        const PANEL_ROWS: usize,
457        const PANEL_COLS: usize,
458        const NROWS: usize,
459        const BITS: u8,
460        const FRAME_COUNT: usize,
461        const TILE_ROWS: usize,
462        const TILE_COLS: usize,
463        const FB_COLS: usize,
464    > MutableFrameBuffer
465    for TiledFrameBuffer<
466        F,
467        M,
468        PANEL_ROWS,
469        PANEL_COLS,
470        NROWS,
471        BITS,
472        FRAME_COUNT,
473        TILE_ROWS,
474        TILE_COLS,
475        FB_COLS,
476    >
477{
478}
479
480#[cfg(test)]
481mod tests {
482    extern crate std;
483
484    use embedded_graphics::prelude::*;
485
486    use super::*;
487    use crate::MutableFrameBuffer;
488    use core::convert::Infallible;
489
490    #[test]
491    fn test_virtual_size_function_with_equal_rows_and_cols() {
492        const ROWS_IN_PANEL: usize = 32;
493        const COLS_IN_PANEL: usize = 64;
494        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 3>;
495        let virt_size = PanelChain::virtual_size();
496        assert_eq!(virt_size, (ROWS_IN_PANEL * 3, COLS_IN_PANEL * 3));
497    }
498
499    #[test]
500    fn test_virtual_size_function_with_uneven_rows_and_cols() {
501        const ROWS_IN_PANEL: usize = 32;
502        const COLS_IN_PANEL: usize = 64;
503        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 5, 3>;
504        let virt_size = PanelChain::virtual_size();
505        assert_eq!(virt_size, (ROWS_IN_PANEL * 5, COLS_IN_PANEL * 3));
506    }
507
508    #[test]
509    fn test_virtual_size_function_with_single_column() {
510        const ROWS_IN_PANEL: usize = 32;
511        const COLS_IN_PANEL: usize = 64;
512        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 1>;
513        let virt_size = PanelChain::virtual_size();
514        assert_eq!(virt_size, (ROWS_IN_PANEL * 3, COLS_IN_PANEL));
515    }
516
517    #[test]
518    fn test_fb_size_function_with_equal_rows_and_cols() {
519        const ROWS_IN_PANEL: usize = 32;
520        const COLS_IN_PANEL: usize = 64;
521        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 3>;
522        let virt_size = PanelChain::fb_size();
523        assert_eq!(virt_size, (ROWS_IN_PANEL, COLS_IN_PANEL * 9));
524    }
525
526    #[test]
527    fn test_fb_size_function_with_uneven_rows_and_cols() {
528        const ROWS_IN_PANEL: usize = 32;
529        const COLS_IN_PANEL: usize = 64;
530        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 5, 3>;
531        let virt_size = PanelChain::fb_size();
532        assert_eq!(virt_size, (ROWS_IN_PANEL, COLS_IN_PANEL * 15));
533    }
534
535    #[test]
536    fn test_fb_size_function_with_single_column() {
537        const ROWS_IN_PANEL: usize = 32;
538        const COLS_IN_PANEL: usize = 64;
539        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 1>;
540        let virt_size = PanelChain::fb_size();
541        assert_eq!(virt_size, (ROWS_IN_PANEL, COLS_IN_PANEL * 3));
542    }
543
544    #[test]
545    fn test_pixel_remap_top_right_down_point_in_origin() {
546        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
547
548        let pixel = PanelChain::remap(Pixel(Point::new(0, 0), Color::RED));
549        assert_eq!(pixel.0, Point::new(384, 0));
550    }
551
552    #[test]
553    fn test_pixel_remap_top_right_down_point_in_bottom_left_corner() {
554        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
555
556        let pixel = PanelChain::remap(Pixel(Point::new(0, 95), Color::RED));
557        assert_eq!(pixel.0, Point::new(0, 31));
558    }
559
560    #[test]
561    fn test_pixel_remap_top_right_down_point_in_bottom_right_corner() {
562        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
563
564        let pixel = PanelChain::remap(Pixel(Point::new(191, 95), Color::RED));
565        assert_eq!(pixel.0, Point::new(191, 31));
566    }
567
568    #[test]
569    fn test_pixel_remap_top_right_down_point_on_x_right_edge_of_first_panel() {
570        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
571
572        let pixel = PanelChain::remap(Pixel(Point::new(63, 0), Color::RED));
573        assert_eq!(pixel.0, Point::new(447, 0));
574    }
575
576    #[test]
577    fn test_pixel_remap_top_right_down_point_on_x_left_edge_of_second_panel() {
578        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
579
580        let pixel = PanelChain::remap(Pixel(Point::new(64, 0), Color::RED));
581        assert_eq!(pixel.0, Point::new(448, 0));
582    }
583
584    #[test]
585    fn test_pixel_remap_top_right_down_point_on_y_bottom_edge_of_first_panel() {
586        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
587
588        let pixel = PanelChain::remap(Pixel(Point::new(0, 31), Color::RED));
589        assert_eq!(pixel.0, Point::new(384, 31));
590    }
591
592    #[test]
593    fn test_pixel_remap_top_right_down_point_on_y_top_edge_of_fourth_panel() {
594        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
595
596        let pixel = PanelChain::remap(Pixel(Point::new(0, 32), Color::RED));
597        assert_eq!(pixel.0, Point::new(383, 31));
598    }
599
600    #[test]
601    fn test_pixel_remap_top_right_down_point_slightly_to_the_top_middle() {
602        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
603
604        let pixel = PanelChain::remap(Pixel(Point::new(100, 40), Color::RED));
605        assert_eq!(pixel.0, Point::new(283, 23));
606    }
607
608    #[test]
609    fn test_pixel_remap_negative_pixel_does_not_remap() {
610        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
611
612        let pixel = PanelChain::remap(Pixel(Point::new(-5, 40), Color::RED));
613        assert_eq!(pixel.0, Point::new(-5, 40));
614    }
615
616    #[test]
617    fn test_compute_tiled_cols() {
618        assert_eq!(192, compute_tiled_cols(32, 3, 2));
619    }
620
621    #[test]
622    fn test_tiling_framebuffer_canvas_size() {
623        use crate::plain::DmaFrameBuffer;
624        use crate::tiling::{compute_tiled_cols, ChainTopRightDown, TiledFrameBuffer};
625        use crate::{compute_frame_count, compute_rows};
626
627        const TILED_COLS: usize = 3;
628        const TILED_ROWS: usize = 3;
629        const ROWS: usize = 32;
630        const PANEL_COLS: usize = 64;
631        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
632        const BITS: u8 = 2;
633        const NROWS: usize = compute_rows(ROWS);
634        const FRAME_COUNT: usize = compute_frame_count(BITS);
635
636        type FBType = DmaFrameBuffer<ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>;
637        type TiledFBType = TiledFrameBuffer<
638            FBType,
639            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
640            ROWS,
641            PANEL_COLS,
642            NROWS,
643            BITS,
644            FRAME_COUNT,
645            TILED_ROWS,
646            TILED_COLS,
647            FB_COLS,
648        >;
649
650        let fb = TiledFBType::new();
651
652        assert_eq!(fb.size(), Size::new(192, 96));
653    }
654
655    // Test helper framebuffer that records calls for verification
656    struct TestFrameBuffer {
657        calls: std::cell::RefCell<std::vec::Vec<Call>>,
658        buf: [u8; 8],
659    }
660
661    #[derive(Debug, Clone, PartialEq, Eq)]
662    enum Call {
663        Erase,
664        SetPixel { p: Point, color: Color },
665        Draw(std::vec::Vec<(Point, Color)>),
666    }
667
668    impl TestFrameBuffer {
669        fn new() -> Self {
670            Self {
671                calls: std::cell::RefCell::new(std::vec::Vec::new()),
672                buf: [0; 8],
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()
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        type Word = u8;
709
710        fn plane_count(&self) -> usize {
711            1
712        }
713
714        fn plane_ptr_len(&self, _plane_idx: usize) -> (*const u8, usize) {
715            (self.buf.as_ptr(), self.buf.len())
716        }
717    }
718
719    impl FrameBufferOperations for TestFrameBuffer {
720        fn erase(&mut self) {
721            self.calls.borrow_mut().push(Call::Erase);
722        }
723
724        fn set_pixel(&mut self, p: Point, color: Color) {
725            self.calls.borrow_mut().push(Call::SetPixel { p, color });
726        }
727    }
728
729    impl MutableFrameBuffer for TestFrameBuffer {}
730
731    unsafe impl embedded_dma::ReadBuffer for TestFrameBuffer {
732        type Word = u8;
733
734        unsafe fn read_buffer(&self) -> (*const u8, usize) {
735            (self.buf.as_ptr(), self.buf.len())
736        }
737    }
738
739    #[test]
740    fn test_tiled_draw_iter_forwards_with_remap() {
741        const TILED_COLS: usize = 3;
742        const TILED_ROWS: usize = 3;
743        const ROWS: usize = 32;
744        const PANEL_COLS: usize = 64;
745        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
746
747        let mut fb = TiledFrameBuffer::<
748            TestFrameBuffer,
749            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
750            ROWS,
751            PANEL_COLS,
752            { crate::compute_rows(ROWS) },
753            2,
754            { crate::compute_frame_count(2) },
755            TILED_ROWS,
756            TILED_COLS,
757            FB_COLS,
758        >(TestFrameBuffer::new(), core::marker::PhantomData);
759
760        let input = [
761            Pixel(Point::new(0, 0), Color::RED),
762            Pixel(Point::new(63, 0), Color::GREEN),
763            Pixel(Point::new(64, 0), Color::BLUE),
764            Pixel(Point::new(100, 40), Color::WHITE),
765        ];
766
767        fb.draw_iter(input.into_iter()).unwrap();
768
769        let calls = fb.0.take_calls();
770        assert_eq!(calls.len(), 1);
771        match &calls[0] {
772            Call::Draw(v) => {
773                let expected =
774                    [
775                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
776                            Pixel(Point::new(0, 0), Color::RED),
777                        ),
778                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
779                            Pixel(Point::new(63, 0), Color::GREEN),
780                        ),
781                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
782                            Pixel(Point::new(64, 0), Color::BLUE),
783                        ),
784                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
785                            Pixel(Point::new(100, 40), Color::WHITE),
786                        ),
787                    ];
788                let expected_points: std::vec::Vec<(Point, Color)> =
789                    expected.iter().map(|p| (p.0, p.1)).collect();
790                assert_eq!(v.as_slice(), expected_points.as_slice());
791            }
792            _ => panic!("expected a Draw call"),
793        }
794    }
795
796    #[test]
797    fn test_tiled_set_pixel_remaps_and_forwards() {
798        const TILED_COLS: usize = 3;
799        const TILED_ROWS: usize = 3;
800        const ROWS: usize = 32;
801        const PANEL_COLS: usize = 64;
802        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
803
804        let mut fb = TiledFrameBuffer::<
805            TestFrameBuffer,
806            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
807            ROWS,
808            PANEL_COLS,
809            { crate::compute_rows(ROWS) },
810            2,
811            { crate::compute_frame_count(2) },
812            TILED_ROWS,
813            TILED_COLS,
814            FB_COLS,
815        >(TestFrameBuffer::new(), core::marker::PhantomData);
816
817        let p = Point::new(100, 40);
818        fb.set_pixel(p, Color::BLUE);
819
820        let calls = fb.0.take_calls();
821        assert_eq!(calls.len(), 1);
822        match calls.into_iter().next().unwrap() {
823            Call::SetPixel { p: rp, color } => {
824                let expected =
825                    ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap_point(p);
826                assert_eq!(rp, expected);
827                assert_eq!(color, Color::BLUE);
828            }
829            _ => panic!("expected a SetPixel call"),
830        }
831    }
832
833    #[test]
834    fn test_tiled_erase_forwards() {
835        const TILED_COLS: usize = 2;
836        const TILED_ROWS: usize = 2;
837        const ROWS: usize = 32;
838        const PANEL_COLS: usize = 64;
839        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
840
841        let mut fb = TiledFrameBuffer::<
842            TestFrameBuffer,
843            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
844            ROWS,
845            PANEL_COLS,
846            { crate::compute_rows(ROWS) },
847            2,
848            { crate::compute_frame_count(2) },
849            TILED_ROWS,
850            TILED_COLS,
851            FB_COLS,
852        >(TestFrameBuffer::new(), core::marker::PhantomData);
853        fb.erase();
854        let calls = fb.0.take_calls();
855        assert_eq!(calls, std::vec![Call::Erase]);
856    }
857
858    #[test]
859    fn test_tiled_negative_coordinates_not_remapped() {
860        const TILED_COLS: usize = 2;
861        const TILED_ROWS: usize = 2;
862        const ROWS: usize = 32;
863        const PANEL_COLS: usize = 64;
864        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
865
866        let mut fb = TiledFrameBuffer::<
867            TestFrameBuffer,
868            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
869            ROWS,
870            PANEL_COLS,
871            { crate::compute_rows(ROWS) },
872            2,
873            { crate::compute_frame_count(2) },
874            TILED_ROWS,
875            TILED_COLS,
876            FB_COLS,
877        >(TestFrameBuffer::new(), core::marker::PhantomData);
878
879        // set_pixel path
880        let neg = Point::new(-3, 5);
881        fb.set_pixel(neg, Color::GREEN);
882        // draw_iter path
883        fb.draw_iter(core::iter::once(Pixel(Point::new(10, -2), Color::RED)))
884            .unwrap();
885
886        let calls = fb.0.take_calls();
887        assert_eq!(calls.len(), 2);
888        assert!(matches!(calls[0], Call::SetPixel { p, .. } if p == neg));
889        match &calls[1] {
890            Call::Draw(v) => {
891                assert_eq!(v.as_slice(), &[(Point::new(10, -2), Color::RED)]);
892            }
893            _ => panic!("expected a Draw call"),
894        }
895    }
896
897    #[test]
898    fn test_tiled_read_buffer_passthrough() {
899        const TILED_COLS: usize = 2;
900        const TILED_ROWS: usize = 2;
901        const ROWS: usize = 32;
902        const PANEL_COLS: usize = 64;
903        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
904
905        let fb = TiledFrameBuffer::<
906            TestFrameBuffer,
907            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
908            ROWS,
909            PANEL_COLS,
910            { crate::compute_rows(ROWS) },
911            2,
912            { crate::compute_frame_count(2) },
913            TILED_ROWS,
914            TILED_COLS,
915            FB_COLS,
916        >(TestFrameBuffer::new(), core::marker::PhantomData);
917
918        let inner_ptr = fb.0.buf.as_ptr();
919        let inner_len = fb.0.buf.len();
920
921        let (ptr, len) = unsafe { fb.read_buffer() };
922        assert_eq!(ptr, inner_ptr);
923        assert_eq!(len, inner_len);
924    }
925
926    #[test]
927    fn test_tiled_get_word_size_passthrough() {
928        const TILED_COLS: usize = 2;
929        const TILED_ROWS: usize = 2;
930        const ROWS: usize = 32;
931        const PANEL_COLS: usize = 64;
932        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
933
934        let fb = TiledFrameBuffer::<
935            TestFrameBuffer,
936            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
937            ROWS,
938            PANEL_COLS,
939            { crate::compute_rows(ROWS) },
940            2,
941            { crate::compute_frame_count(2) },
942            TILED_ROWS,
943            TILED_COLS,
944            FB_COLS,
945        >(TestFrameBuffer::new(), core::marker::PhantomData);
946        assert_eq!(fb.get_word_size(), WordSize::Eight);
947    }
948
949    #[test]
950    fn test_tiled_get_word_size_eight_passthrough() {
951        const TILED_COLS: usize = 2;
952        const TILED_ROWS: usize = 2;
953        const ROWS: usize = 32;
954        const PANEL_COLS: usize = 64;
955        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
956
957        let fb = TiledFrameBuffer::<
958            TestFrameBuffer,
959            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
960            ROWS,
961            PANEL_COLS,
962            { crate::compute_rows(ROWS) },
963            2,
964            { crate::compute_frame_count(2) },
965            TILED_ROWS,
966            TILED_COLS,
967            FB_COLS,
968        >(TestFrameBuffer::new(), core::marker::PhantomData);
969        assert_eq!(fb.get_word_size(), WordSize::Eight);
970    }
971
972    // Remapper that generates very large coordinates to trigger u16 truncation in remap_point
973    struct Huge<
974        const PANEL_ROWS: usize,
975        const PANEL_COLS: usize,
976        const TILE_ROWS: usize,
977        const TILE_COLS: usize,
978    >;
979
980    impl<
981            const PANEL_ROWS: usize,
982            const PANEL_COLS: usize,
983            const TILE_ROWS: usize,
984            const TILE_COLS: usize,
985        > PixelRemapper for Huge<PANEL_ROWS, PANEL_COLS, TILE_ROWS, TILE_COLS>
986    {
987        const VIRT_ROWS: usize = PANEL_ROWS * TILE_ROWS;
988        const VIRT_COLS: usize = PANEL_COLS * TILE_COLS;
989        const FB_ROWS: usize = PANEL_ROWS;
990        const FB_COLS: usize = PANEL_COLS * TILE_ROWS * TILE_COLS;
991
992        fn remap_xy(x: usize, y: usize) -> (usize, usize) {
993            (x + 70_000, y + 70_000)
994        }
995    }
996
997    #[test]
998    fn test_remap_point_truncates_to_u16_range() {
999        const TILED_COLS: usize = 1;
1000        const TILED_ROWS: usize = 1;
1001        const ROWS: usize = 32;
1002        const PANEL_COLS: usize = 64;
1003        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1004
1005        let mut fb = TiledFrameBuffer::<
1006            TestFrameBuffer,
1007            Huge<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1008            ROWS,
1009            PANEL_COLS,
1010            { crate::compute_rows(ROWS) },
1011            2,
1012            { crate::compute_frame_count(2) },
1013            TILED_ROWS,
1014            TILED_COLS,
1015            FB_COLS,
1016        >(TestFrameBuffer::new(), core::marker::PhantomData);
1017        fb.set_pixel(Point::new(1, 2), Color::RED);
1018
1019        let calls = fb.0.take_calls();
1020        match calls.into_iter().next().unwrap() {
1021            Call::SetPixel { p, color } => {
1022                let (rx, ry) = (1usize + 70_000, 2usize + 70_000);
1023                let expected = Point::new(i32::from(rx as u16), i32::from(ry as u16));
1024                assert_eq!(p, expected);
1025                assert_eq!(color, Color::RED);
1026            }
1027            other => panic!("unexpected call recorded: {other:?}"),
1028        }
1029    }
1030
1031    #[test]
1032    fn test_more_compute_tiled_cols_cases() {
1033        assert_eq!(compute_tiled_cols(64, 1, 4), 256);
1034        assert_eq!(compute_tiled_cols(64, 4, 1), 256);
1035        assert_eq!(compute_tiled_cols(32, 4, 5), 640);
1036    }
1037
1038    #[test]
1039    fn test_tiled_default_and_new_construct() {
1040        const TILED_COLS: usize = 4;
1041        const TILED_ROWS: usize = 2;
1042        const ROWS: usize = 32;
1043        const PANEL_COLS: usize = 64;
1044        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1045
1046        let fb_default = TiledFrameBuffer::<
1047            TestFrameBuffer,
1048            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1049            ROWS,
1050            PANEL_COLS,
1051            { crate::compute_rows(ROWS) },
1052            2,
1053            { crate::compute_frame_count(2) },
1054            TILED_ROWS,
1055            TILED_COLS,
1056            FB_COLS,
1057        >::default();
1058
1059        let fb_new = TiledFrameBuffer::<
1060            TestFrameBuffer,
1061            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1062            ROWS,
1063            PANEL_COLS,
1064            { crate::compute_rows(ROWS) },
1065            2,
1066            { crate::compute_frame_count(2) },
1067            TILED_ROWS,
1068            TILED_COLS,
1069            FB_COLS,
1070        >::new();
1071
1072        // Default constructs inner TestFrameBuffer::default()
1073        assert_eq!(fb_default.get_word_size(), WordSize::Eight);
1074        assert_eq!(fb_new.get_word_size(), WordSize::Eight);
1075
1076        // Size comes from OriginDimensions impl on TiledFrameBuffer (via M::virtual_size)
1077        let expected_size = Size::new((PANEL_COLS * TILED_COLS) as u32, (ROWS * TILED_ROWS) as u32);
1078        assert_eq!(fb_default.size(), expected_size);
1079        assert_eq!(fb_new.size(), expected_size);
1080
1081        // No calls recorded yet on inner framebuffer
1082        assert!(fb_default.0.take_calls().is_empty());
1083        assert!(fb_new.0.take_calls().is_empty());
1084    }
1085
1086    #[test]
1087    fn test_tiled_origin_dimensions_matches_virtual_size() {
1088        const TILED_COLS: usize = 5;
1089        const TILED_ROWS: usize = 2;
1090        const ROWS: usize = 32;
1091        const PANEL_COLS: usize = 64;
1092        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1093
1094        let fb = TiledFrameBuffer::<
1095            TestFrameBuffer,
1096            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1097            ROWS,
1098            PANEL_COLS,
1099            { crate::compute_rows(ROWS) },
1100            2,
1101            { crate::compute_frame_count(2) },
1102            TILED_ROWS,
1103            TILED_COLS,
1104            FB_COLS,
1105        >::new();
1106
1107        let (virt_rows, virt_cols) = <ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS> as PixelRemapper>::virtual_size();
1108        assert_eq!(fb.size(), Size::new(virt_cols as u32, virt_rows as u32));
1109    }
1110
1111    // Expected mapping for ChainTopRightDown (current, correct behavior)
1112    fn expected_ctrdd_xy<const PR: usize, const PC: usize, const TR: usize, const TC: usize>(
1113        x: usize,
1114        y: usize,
1115    ) -> (usize, usize) {
1116        let row = y / PR;
1117        let base = (TR - 1 - row) * (PC * TC);
1118        if row % 2 == 1 {
1119            (base + (PC * TC) - 1 - x, PR - 1 - (y % PR))
1120        } else {
1121            (base + x, y % PR)
1122        }
1123    }
1124
1125    #[test]
1126    fn test_chain_top_right_down_corners_2x3() {
1127        const PR: usize = 32;
1128        const PC: usize = 64;
1129        const TR: usize = 2;
1130        const TC: usize = 3;
1131        type M = ChainTopRightDown<PR, PC, TR, TC>;
1132
1133        for r in 0..TR {
1134            for c in 0..TC {
1135                let x0 = c * PC;
1136                let y0 = r * PR;
1137                let corners = [
1138                    (x0, y0),                   // TL
1139                    (x0 + PC - 1, y0),          // TR
1140                    (x0, y0 + PR - 1),          // BL
1141                    (x0 + PC - 1, y0 + PR - 1), // BR
1142                ];
1143
1144                for &(x, y) in &corners {
1145                    let got = <M as PixelRemapper>::remap_xy(x, y);
1146                    let exp = expected_ctrdd_xy::<PR, PC, TR, TC>(x, y);
1147                    assert_eq!(
1148                        got, exp,
1149                        "corner mismatch at panel (row={}, col={}), virtual=({}, {})",
1150                        r, c, x, y
1151                    );
1152                }
1153            }
1154        }
1155    }
1156
1157    #[test]
1158    fn test_chain_top_right_down_corners_3x2() {
1159        const PR: usize = 32;
1160        const PC: usize = 64;
1161        const TR: usize = 3;
1162        const TC: usize = 2;
1163        type M = ChainTopRightDown<PR, PC, TR, TC>;
1164
1165        for r in 0..TR {
1166            for c in 0..TC {
1167                let x0 = c * PC;
1168                let y0 = r * PR;
1169                let corners = [
1170                    (x0, y0),                   // TL
1171                    (x0 + PC - 1, y0),          // TR
1172                    (x0, y0 + PR - 1),          // BL
1173                    (x0 + PC - 1, y0 + PR - 1), // BR
1174                ];
1175
1176                for &(x, y) in &corners {
1177                    let got = <M as PixelRemapper>::remap_xy(x, y);
1178                    let exp = expected_ctrdd_xy::<PR, PC, TR, TC>(x, y);
1179                    assert_eq!(
1180                        got, exp,
1181                        "corner mismatch at panel (row={}, col={}), virtual=({}, {})",
1182                        r, c, x, y
1183                    );
1184                }
1185            }
1186        }
1187    }
1188}