Skip to main content

hub75_framebuffer/
tiling.rs

1//! Coordinate remapping for tiled and scan-pattern LED panel arrangements
2//!
3//! Use [`RemappedFrameBuffer`] to wrap any framebuffer type (`plain`, `latched`,
4//! `bitplane::plain`, `bitplane::latched`) and remap pixel coordinates through a
5//! [`PixelRemapper`] implementation.
6//!
7//! Available remappers:
8//! - [`ChainTopRightDown`] — tiles multiple panels into a larger virtual display
9//! - [`QuarterScan`] — remaps for 1/16-scan (quarter-scan) 64×64 panels, with
10//!   pluggable wiring variants (see [`quarter_scan`])
11//!
12//! The older [`TiledFrameBuffer`] is still available but deprecated in favour of
13//! [`RemappedFrameBuffer`], which has a simpler generic signature and works with
14//! all framebuffer types.
15
16use core::{convert::Infallible, marker::PhantomData};
17
18use crate::{
19    BcmLenReps, Color, FrameBuffer, FrameBufferOperations, MutableFrameBuffer,
20    BCM_SEQUENCE_CAPACITY,
21};
22use embedded_graphics::prelude::{DrawTarget, OriginDimensions, PixelColor, Point, Size};
23
24/// Computes the number of columns needed if the displays are being tiled together.
25/// # Arguments
26///
27/// * `cols` - Number of columns per panel
28/// * `num_panels_wide` - Number of panels tiled horizontally
29/// * `num_panels_high` - Number of panels tiled vertically
30///
31/// # Returns
32///
33/// Number of columns needed internally for `DmaFrameBuffer`
34#[must_use]
35pub const fn compute_tiled_cols(
36    cols: usize,
37    num_panels_wide: usize,
38    num_panels_high: usize,
39) -> usize {
40    cols * num_panels_wide * num_panels_high
41}
42
43/// Trait for pixel re-mappers
44///
45/// Implementors of this trait will remap x,y coordinates from a
46/// virtual panel to the actual framebuffer used to drive the panels
47///
48/// # Type Parameters
49///
50/// * `PANEL_ROWS` - Number of rows in a single panel
51/// * `PANEL_COLS` - Number of columns in a single panel
52/// * `TILE_ROWS` - Number of panels stacked vertically
53/// * `TILE_COLS` - Number of panels stacked horizontally
54pub trait PixelRemapper {
55    /// Number of rows in the virtual panel
56    const VIRT_ROWS: usize;
57    /// Number of columns in the virtual panel
58    const VIRT_COLS: usize;
59    /// Number of rows in the actual framebuffer
60    const FB_ROWS: usize;
61    /// Number of columns in the actual framebuffer
62    const FB_COLS: usize;
63
64    /// Remap a virtual pixel to a framebuffer pixel
65    #[inline]
66    fn remap<C: PixelColor>(mut pixel: embedded_graphics::Pixel<C>) -> embedded_graphics::Pixel<C> {
67        pixel.0 = Self::remap_point(pixel.0);
68        pixel
69    }
70
71    /// Remap a virtual point to a framebuffer point
72    #[inline]
73    #[must_use]
74    fn remap_point(mut point: Point) -> Point {
75        if point.x < 0 || point.y < 0 {
76            // Skip remapping points which are off the screen
77            return point;
78        }
79        let (re_x, re_y) = Self::remap_xy(point.x as usize, point.y as usize);
80        // If larger than u16, it is fair to assume that the point will be off the screen
81        point.x = i32::from(re_x as u16);
82        point.y = i32::from(re_y as u16);
83        point
84    }
85
86    /// Remap an x,y coordinate to a framebuffer pixel
87    fn remap_xy(x: usize, y: usize) -> (usize, usize);
88
89    /// Size of the virtual panel
90    #[inline]
91    #[must_use]
92    fn virtual_size() -> (usize, usize) {
93        (Self::VIRT_ROWS, Self::VIRT_COLS)
94    }
95
96    /// Size of the framebuffer that this remaps to
97    #[inline]
98    #[must_use]
99    fn fb_size() -> (usize, usize) {
100        (Self::FB_ROWS, Self::FB_COLS)
101    }
102}
103
104/// Chaining strategy for tiled panels
105///
106/// This type should be provided to the [`TiledFrameBuffer`] as a type argument.
107/// Take a look at its documentation for more details
108///
109/// When looking at the front, panels are chained together starting at the top right, chaining to the
110/// left until the end of the column. Then wrapping down to the next row where panels are chained left to right.
111/// This makes every second rows panels installed upside down.
112/// This pattern repeats until all rows of panels are covered.
113///
114/// # Type Parameters
115///
116/// * `PANEL_ROWS` - Number of rows in a single panel
117/// * `PANEL_COLS` - Number of columns in a single panel
118/// * `TILE_ROWS` - Number of panels stacked vertically
119/// * `TILE_COLS` - Number of panels stacked horizontally
120#[cfg_attr(feature = "defmt", derive(defmt::Format))]
121#[derive(core::fmt::Debug)]
122pub struct ChainTopRightDown<
123    const PANEL_ROWS: usize,
124    const PANEL_COLS: usize,
125    const TILE_ROWS: usize,
126    const TILE_COLS: usize,
127> {}
128
129impl<
130        const PANEL_ROWS: usize,
131        const PANEL_COLS: usize,
132        const TILE_ROWS: usize,
133        const TILE_COLS: usize,
134    > PixelRemapper for ChainTopRightDown<PANEL_ROWS, PANEL_COLS, TILE_ROWS, TILE_COLS>
135{
136    const VIRT_ROWS: usize = PANEL_ROWS * TILE_ROWS;
137    const VIRT_COLS: usize = PANEL_COLS * TILE_COLS;
138    const FB_ROWS: usize = PANEL_ROWS;
139    const FB_COLS: usize = PANEL_COLS * TILE_ROWS * TILE_COLS;
140
141    fn remap_xy(x: usize, y: usize) -> (usize, usize) {
142        // 0 = top row, 1 = next row, …
143        let row = y / PANEL_ROWS;
144        let base = (TILE_ROWS - 1 - row) * Self::VIRT_COLS;
145
146        if row % 2 == 1 {
147            // this row is mounted upside-down
148            (
149                base + Self::VIRT_COLS - 1 - x, // mirror x across the whole virtual row
150                PANEL_ROWS - 1 - (y % PANEL_ROWS), // flip y within the panel
151            )
152        } else {
153            (base + x, y % PANEL_ROWS) // normal orientation
154        }
155    }
156}
157
158/// Slot-assignment variants for [`QuarterScan`] panels
159///
160/// A quarter-scan panel's four row groups can be wired to the four
161/// (channel, section) slots of the shift register in different orders
162/// depending on the driver chip and manufacturer.  Each variant below
163/// describes one common wiring; pick the one that matches your panel and
164/// supply it as the third type parameter of `QuarterScan`, e.g.
165/// `QuarterScan<64, 64, quarter_scan::Linear>`.
166///
167/// Panels with exotic wirings are supported without forking this crate:
168/// implement [`Variant`](crate::tiling::quarter_scan::Variant) on a local
169/// marker type.
170pub mod quarter_scan {
171    /// Group → (channel, section) slot assignment for a quarter-scan panel
172    ///
173    /// The table is indexed by row group (`y / (PANEL_ROWS / 4)`); each entry
174    /// is `(channel, section)` where channel 0 is the top framebuffer band
175    /// (framebuffer rows `0..PANEL_ROWS / 4`, channel 1 the bottom band) and
176    /// section selects which `PANEL_COLS`-wide half of that channel's shift
177    /// register the group is wired to.
178    pub trait Variant {
179        /// Group → (channel, section) slot table
180        const SLOT: [(usize, usize); 4];
181    }
182
183    /// Naive row order: group 0 → ch1/sec0, 1 → ch1/sec1, 2 → ch2/sec0, 3 → ch2/sec1
184    #[cfg_attr(feature = "defmt", derive(defmt::Format))]
185    #[derive(core::fmt::Debug)]
186    pub struct Linear;
187
188    impl Variant for Linear {
189        const SLOT: [(usize, usize); 4] = [(0, 0), (0, 1), (1, 0), (1, 1)];
190    }
191
192    /// Sections swapped within each half (the default): group 0 → ch1/sec1,
193    /// 1 → ch1/sec0, 2 → ch2/sec1, 3 → ch2/sec0.  Verified on hardware.
194    #[cfg_attr(feature = "defmt", derive(defmt::Format))]
195    #[derive(core::fmt::Debug)]
196    pub struct SectionsSwapped;
197
198    impl Variant for SectionsSwapped {
199        const SLOT: [(usize, usize); 4] = [(0, 1), (0, 0), (1, 1), (1, 0)];
200    }
201
202    /// Top and bottom halves exchanged: group 0 → ch2/sec0, 1 → ch2/sec1,
203    /// 2 → ch1/sec0, 3 → ch1/sec1
204    #[cfg_attr(feature = "defmt", derive(defmt::Format))]
205    #[derive(core::fmt::Debug)]
206    pub struct HalvesSwapped;
207
208    impl Variant for HalvesSwapped {
209        const SLOT: [(usize, usize); 4] = [(1, 0), (1, 1), (0, 0), (0, 1)];
210    }
211
212    /// Channel-interleaved groups: group 0 → ch1/sec0, 1 → ch2/sec0,
213    /// 2 → ch1/sec1, 3 → ch2/sec1
214    #[cfg_attr(feature = "defmt", derive(defmt::Format))]
215    #[derive(core::fmt::Debug)]
216    pub struct Alternating;
217
218    impl Variant for Alternating {
219        const SLOT: [(usize, usize); 4] = [(0, 0), (1, 0), (0, 1), (1, 1)];
220    }
221}
222
223/// Pixel remapper for "quarter-scan" panels (1/16-scan on 64×64 panels)
224///
225/// Some LED panels — particularly 64×64 modules — use fewer row-address steps
226/// than the row count would suggest.  A 1/16-scan 64×64 panel has only 16 row
227/// addresses (A–D) instead of the 32 a 1/32-scan panel would use, and lights
228/// four rows at a time: rows *n*, *n+16*, *n+32* and *n+48* for address *n*.
229/// To make this work with the two HUB75 data channels, the panel internally
230/// wires each channel's shift register as two side-by-side 64-column
231/// sections, so the physical framebuffer is 2× wider and 2× shorter than the
232/// logical display (32×128 for a 64×64 panel).
233///
234/// The mapping splits the panel into four groups of `PANEL_ROWS / 4`
235/// consecutive rows and places them side by side.  How the four groups are
236/// wired to the four (channel, section) slots varies by driver chip and
237/// manufacturer (FM6126, ICN2037, MBI5153, etc.); the `V` type parameter
238/// selects the wiring — see the [`quarter_scan`] module for the built-in
239/// variants.  The default [`quarter_scan::SectionsSwapped`] (verified on
240/// hardware) maps:
241///
242/// | virtual rows  | channel    | section (framebuffer columns) |
243/// |---------------|------------|-------------------------------|
244/// | `0..16`       | 1 (top)    | 1 (`64..128`)                 |
245/// | `16..32`      | 1 (top)    | 0 (`0..64`)                   |
246/// | `32..48`      | 2 (bottom) | 1 (`64..128`)                 |
247/// | `48..64`      | 2 (bottom) | 0 (`0..64`)                   |
248///
249/// # Type Parameters
250///
251/// * `PANEL_ROWS` — Logical row count of the panel (e.g. 64)
252/// * `PANEL_COLS` — Logical column count of the panel (e.g. 64)
253/// * `V` — Group → slot wiring variant (see [`quarter_scan`]); defaults to
254///   [`quarter_scan::SectionsSwapped`]
255///
256/// # Framebuffer geometry
257///
258/// The underlying framebuffer must be allocated with:
259/// - rows = `PANEL_ROWS / 2`  (e.g. 32 for a 64-row panel — 16 addresses × 2 channels)
260/// - cols = `PANEL_COLS * 2`  (e.g. 128 for a 64-column panel — 2 sections per channel)
261///
262/// For the bitplane framebuffers this means
263/// `DmaFrameBuffer<{ PANEL_ROWS / 4 }, { PANEL_COLS * 2 }, PLANES>` because
264/// their `NROWS` parameter counts row-address steps (row pairs).
265#[derive(core::fmt::Debug)]
266pub struct QuarterScan<
267    const PANEL_ROWS: usize,
268    const PANEL_COLS: usize,
269    V: quarter_scan::Variant = quarter_scan::SectionsSwapped,
270> {
271    _variant: PhantomData<V>,
272}
273
274#[cfg(feature = "defmt")]
275impl<const PANEL_ROWS: usize, const PANEL_COLS: usize, V: quarter_scan::Variant> defmt::Format
276    for QuarterScan<PANEL_ROWS, PANEL_COLS, V>
277{
278    fn format(&self, fmt: defmt::Formatter) {
279        defmt::write!(fmt, "QuarterScan");
280    }
281}
282
283impl<const PANEL_ROWS: usize, const PANEL_COLS: usize, V: quarter_scan::Variant> PixelRemapper
284    for QuarterScan<PANEL_ROWS, PANEL_COLS, V>
285{
286    const VIRT_ROWS: usize = PANEL_ROWS;
287    const VIRT_COLS: usize = PANEL_COLS;
288    const FB_ROWS: usize = PANEL_ROWS / 2;
289    const FB_COLS: usize = PANEL_COLS * 2;
290
291    fn remap_xy(x: usize, y: usize) -> (usize, usize) {
292        // Clip points outside the virtual panel: embedded-graphics routinely
293        // generates these (e.g. text with its baseline on the last row has
294        // glyph cells extending below it).  Map them just past the end of
295        // the framebuffer where the inner framebuffer silently discards them.
296        if x >= Self::VIRT_COLS || y >= Self::VIRT_ROWS {
297            return (Self::FB_COLS, Self::FB_ROWS);
298        }
299
300        // Each quarter of the panel (16 consecutive rows on a 64-row panel)
301        // is wired to one 64-column section of the shift register; the
302        // variant's table gives the group → (channel, section) wiring.
303        let group_rows = PANEL_ROWS / 4;
304        let group = y / group_rows;
305        let addr = y % group_rows;
306        let (channel, section) = V::SLOT[group];
307        (section * PANEL_COLS + x, channel * group_rows + addr)
308    }
309}
310
311/// Tile together multiple displays in a certain configuration to form a single larger display
312///
313/// This is a wrapper around an actual framebuffer implementation which can be used to tile multiple
314/// LED matrices together by using a certain pixel remapping strategy.
315///
316/// # Type Parameters
317/// - `F` - The type of the underlying framebuffer which will drive the display
318/// - `M` - The pixel remapping strategy (see implementers of [`PixelRemapper`]) to use to map the virtual framebuffer to the actual framebuffer
319/// - `PANEL_ROWS` - Number of rows in a single panel
320/// - `PANEL_COLS` - Number of columns in a single panel
321/// - `NROWS`: Number of rows per scan (typically half of ROWS)
322/// - `BITS`: Color depth (1-8 bits)
323/// - `FRAME_COUNT`: Number of frames used for Binary Code Modulation
324/// * `TILE_ROWS` - Number of panels stacked vertically
325/// * `TILE_COLS` - Number of panels stacked horizontally
326/// * `FB_COLS` - Number of columns that the actual framebuffer must have to drive all display
327///
328/// # Example
329/// ```rust
330/// use hub75_framebuffer::{compute_frame_count, compute_rows};
331/// use hub75_framebuffer::plain::DmaFrameBuffer;
332/// use hub75_framebuffer::tiling::{TiledFrameBuffer, ChainTopRightDown, compute_tiled_cols};
333///
334/// const TILED_COLS: usize = 3;
335/// const TILED_ROWS: usize = 3;
336/// const ROWS: usize = 32;
337/// const PANEL_COLS: usize = 64;
338/// const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
339/// const BITS: u8 = 2;
340/// const NROWS: usize = compute_rows(ROWS);
341/// const FRAME_COUNT: usize = compute_frame_count(BITS);
342///
343/// type FBType = DmaFrameBuffer<ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>;
344/// type TiledFBType = TiledFrameBuffer<
345///     FBType,
346///     ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
347///     ROWS,
348///     PANEL_COLS,
349///     NROWS,
350///     BITS,
351///     FRAME_COUNT,
352///     TILED_ROWS,
353///     TILED_COLS,
354///     FB_COLS,
355/// >;
356///
357/// let mut fb = TiledFBType::new();
358///
359/// // Now fb is ready to be used and can be treated like one big canvas (192*96 pixels in this example)
360/// ```
361#[deprecated(
362    since = "0.11.0",
363    note = "use RemappedFrameBuffer<F, M> instead -- it works with all framebuffer types and has a simpler signature"
364)]
365#[derive(core::fmt::Debug)]
366pub struct TiledFrameBuffer<
367    F,
368    M: PixelRemapper,
369    const PANEL_ROWS: usize,
370    const PANEL_COLS: usize,
371    const NROWS: usize,
372    const BITS: u8,
373    const FRAME_COUNT: usize,
374    const TILE_ROWS: usize,
375    const TILE_COLS: usize,
376    const FB_COLS: usize,
377>(F, PhantomData<M>);
378
379#[allow(deprecated)]
380impl<
381        F: Default,
382        M: PixelRemapper,
383        const PANEL_ROWS: usize,
384        const PANEL_COLS: usize,
385        const NROWS: usize,
386        const BITS: u8,
387        const FRAME_COUNT: usize,
388        const TILE_ROWS: usize,
389        const TILE_COLS: usize,
390        const FB_COLS: usize,
391    >
392    TiledFrameBuffer<
393        F,
394        M,
395        PANEL_ROWS,
396        PANEL_COLS,
397        NROWS,
398        BITS,
399        FRAME_COUNT,
400        TILE_ROWS,
401        TILE_COLS,
402        FB_COLS,
403    >
404{
405    /// Create a new "virtual display" that takes ownership of the underlying framebuffer
406    /// and remaps any pixels written to it to the correct locations of the underlying framebuffer
407    /// based on the given `PixelRemapper`
408    #[must_use]
409    pub fn new() -> Self {
410        Self(F::default(), PhantomData)
411    }
412}
413
414#[allow(deprecated)]
415impl<
416        F: Default,
417        M: PixelRemapper,
418        const PANEL_ROWS: usize,
419        const PANEL_COLS: usize,
420        const NROWS: usize,
421        const BITS: u8,
422        const FRAME_COUNT: usize,
423        const TILE_ROWS: usize,
424        const TILE_COLS: usize,
425        const FB_COLS: usize,
426    > Default
427    for TiledFrameBuffer<
428        F,
429        M,
430        PANEL_ROWS,
431        PANEL_COLS,
432        NROWS,
433        BITS,
434        FRAME_COUNT,
435        TILE_ROWS,
436        TILE_COLS,
437        FB_COLS,
438    >
439{
440    fn default() -> Self {
441        Self::new()
442    }
443}
444
445#[allow(deprecated)]
446impl<
447        F: DrawTarget<Error = Infallible, Color = Color>,
448        M: PixelRemapper,
449        const PANEL_ROWS: usize,
450        const PANEL_COLS: usize,
451        const NROWS: usize,
452        const BITS: u8,
453        const FRAME_COUNT: usize,
454        const TILE_ROWS: usize,
455        const TILE_COLS: usize,
456        const FB_COLS: usize,
457    > DrawTarget
458    for TiledFrameBuffer<
459        F,
460        M,
461        PANEL_ROWS,
462        PANEL_COLS,
463        NROWS,
464        BITS,
465        FRAME_COUNT,
466        TILE_ROWS,
467        TILE_COLS,
468        FB_COLS,
469    >
470{
471    type Color = Color;
472    type Error = Infallible;
473
474    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
475    where
476        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
477    {
478        self.0.draw_iter(pixels.into_iter().map(M::remap))
479    }
480}
481
482#[allow(deprecated)]
483impl<
484        F: DrawTarget<Error = Infallible, Color = Color>,
485        M: PixelRemapper,
486        const PANEL_ROWS: usize,
487        const PANEL_COLS: usize,
488        const NROWS: usize,
489        const BITS: u8,
490        const FRAME_COUNT: usize,
491        const TILE_ROWS: usize,
492        const TILE_COLS: usize,
493        const FB_COLS: usize,
494    > OriginDimensions
495    for TiledFrameBuffer<
496        F,
497        M,
498        PANEL_ROWS,
499        PANEL_COLS,
500        NROWS,
501        BITS,
502        FRAME_COUNT,
503        TILE_ROWS,
504        TILE_COLS,
505        FB_COLS,
506    >
507{
508    fn size(&self) -> Size {
509        Size::new(M::virtual_size().1 as u32, M::virtual_size().0 as u32)
510    }
511}
512
513#[allow(deprecated)]
514impl<
515        F: FrameBufferOperations + FrameBuffer,
516        M: PixelRemapper,
517        const PANEL_ROWS: usize,
518        const PANEL_COLS: usize,
519        const NROWS: usize,
520        const BITS: u8,
521        const FRAME_COUNT: usize,
522        const TILE_ROWS: usize,
523        const TILE_COLS: usize,
524        const FB_COLS: usize,
525    > FrameBufferOperations
526    for TiledFrameBuffer<
527        F,
528        M,
529        PANEL_ROWS,
530        PANEL_COLS,
531        NROWS,
532        BITS,
533        FRAME_COUNT,
534        TILE_ROWS,
535        TILE_COLS,
536        FB_COLS,
537    >
538{
539    #[inline]
540    fn erase(&mut self) {
541        self.0.erase();
542    }
543
544    #[inline]
545    fn set_pixel(&mut self, p: Point, color: Color) {
546        self.0.set_pixel(M::remap_point(p), color);
547    }
548}
549
550#[allow(deprecated)]
551impl<
552        F: FrameBuffer,
553        M: PixelRemapper,
554        const PANEL_ROWS: usize,
555        const PANEL_COLS: usize,
556        const NROWS: usize,
557        const BITS: u8,
558        const FRAME_COUNT: usize,
559        const TILE_ROWS: usize,
560        const TILE_COLS: usize,
561        const FB_COLS: usize,
562    > FrameBuffer
563    for TiledFrameBuffer<
564        F,
565        M,
566        PANEL_ROWS,
567        PANEL_COLS,
568        NROWS,
569        BITS,
570        FRAME_COUNT,
571        TILE_ROWS,
572        TILE_COLS,
573        FB_COLS,
574    >
575{
576    type Word = F::Word;
577
578    const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY] = F::BCM_SEQUENCE;
579
580    const BCM_SEQUENCE_LEN: usize = F::BCM_SEQUENCE_LEN;
581
582    const BCM_SEQUENCE_COUNT: usize = F::BCM_SEQUENCE_COUNT;
583
584    const BCM_SEGMENTS_PER_GROUP: usize = F::BCM_SEGMENTS_PER_GROUP;
585
586    fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
587        self.0.bcm_segment_ptr(index)
588    }
589}
590
591#[allow(deprecated)]
592impl<
593        F: MutableFrameBuffer,
594        M: PixelRemapper,
595        const PANEL_ROWS: usize,
596        const PANEL_COLS: usize,
597        const NROWS: usize,
598        const BITS: u8,
599        const FRAME_COUNT: usize,
600        const TILE_ROWS: usize,
601        const TILE_COLS: usize,
602        const FB_COLS: usize,
603    > MutableFrameBuffer
604    for TiledFrameBuffer<
605        F,
606        M,
607        PANEL_ROWS,
608        PANEL_COLS,
609        NROWS,
610        BITS,
611        FRAME_COUNT,
612        TILE_ROWS,
613        TILE_COLS,
614        FB_COLS,
615    >
616{
617}
618
619#[cfg(feature = "defmt")]
620#[allow(deprecated)]
621impl<
622        F: defmt::Format,
623        M: PixelRemapper,
624        const PANEL_ROWS: usize,
625        const PANEL_COLS: usize,
626        const NROWS: usize,
627        const BITS: u8,
628        const FRAME_COUNT: usize,
629        const TILE_ROWS: usize,
630        const TILE_COLS: usize,
631        const FB_COLS: usize,
632    > defmt::Format
633    for TiledFrameBuffer<
634        F,
635        M,
636        PANEL_ROWS,
637        PANEL_COLS,
638        NROWS,
639        BITS,
640        FRAME_COUNT,
641        TILE_ROWS,
642        TILE_COLS,
643        FB_COLS,
644    >
645{
646    fn format(&self, fmt: defmt::Formatter) {
647        defmt::write!(fmt, "TiledFrameBuffer({:?})", self.0);
648    }
649}
650
651/// Coordinate-remapping wrapper for any framebuffer
652///
653/// This is a generic wrapper around any framebuffer implementation that remaps
654/// pixel coordinates before forwarding them to the inner framebuffer. It can be
655/// used for:
656///
657/// - **Tiling** multiple panels into a larger virtual display (see [`ChainTopRightDown`])
658/// - **Scan-pattern remapping** for panels with non-standard row interleaving
659///   (e.g. 1/16 scan on 64×64 panels — see [`QuarterScan`])
660///
661/// Unlike [`TiledFrameBuffer`], this wrapper uses only two type parameters and
662/// works with all four framebuffer types (`plain`, `latched`, `bitplane::plain`,
663/// `bitplane::latched`).
664///
665/// # Type Parameters
666/// - `F` — The underlying framebuffer type
667/// - `M` — The pixel remapping strategy (see implementors of [`PixelRemapper`])
668///
669/// # Example with tiled plain framebuffer
670/// ```rust
671/// use hub75_framebuffer::{compute_frame_count, compute_rows};
672/// use hub75_framebuffer::plain::DmaFrameBuffer;
673/// use hub75_framebuffer::tiling::{RemappedFrameBuffer, ChainTopRightDown, compute_tiled_cols};
674///
675/// const TILED_COLS: usize = 3;
676/// const TILED_ROWS: usize = 3;
677/// const ROWS: usize = 32;
678/// const PANEL_COLS: usize = 64;
679/// const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
680/// const BITS: u8 = 2;
681/// const NROWS: usize = compute_rows(ROWS);
682/// const FRAME_COUNT: usize = compute_frame_count(BITS);
683///
684/// type FBType = DmaFrameBuffer<ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>;
685/// type Remapper = ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>;
686/// type Display = RemappedFrameBuffer<FBType, Remapper>;
687///
688/// let mut fb = Display::new();
689/// // fb is a 192×96 virtual canvas
690/// ```
691///
692/// # Example with tiled bitplane framebuffer
693/// ```rust
694/// use hub75_framebuffer::bitplane::plain::DmaFrameBuffer;
695/// use hub75_framebuffer::tiling::{RemappedFrameBuffer, ChainTopRightDown, compute_tiled_cols};
696///
697/// const TILED_COLS: usize = 3;
698/// const TILED_ROWS: usize = 3;
699/// const ROWS: usize = 32;
700/// const NROWS: usize = ROWS / 2;
701/// const PANEL_COLS: usize = 64;
702/// const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
703/// const PLANES: usize = 8;
704///
705/// type FBType = DmaFrameBuffer<NROWS, FB_COLS, PLANES>;
706/// type Remapper = ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>;
707/// type Display = RemappedFrameBuffer<FBType, Remapper>;
708///
709/// let mut fb = Display::new();
710/// // fb is a 192×96 virtual canvas backed by a bitplane framebuffer
711/// ```
712#[cfg_attr(feature = "defmt", derive(defmt::Format))]
713#[derive(core::fmt::Debug)]
714pub struct RemappedFrameBuffer<F, M: PixelRemapper>(F, PhantomData<M>);
715
716impl<F: Default, M: PixelRemapper> RemappedFrameBuffer<F, M> {
717    /// Create a new remapped framebuffer that takes ownership of a
718    /// default-constructed inner framebuffer and remaps any pixel writes
719    /// through the given [`PixelRemapper`].
720    #[must_use]
721    pub fn new() -> Self {
722        Self(F::default(), PhantomData)
723    }
724}
725
726impl<F: Default, M: PixelRemapper> Default for RemappedFrameBuffer<F, M> {
727    fn default() -> Self {
728        Self::new()
729    }
730}
731
732impl<F: DrawTarget<Error = Infallible, Color = Color>, M: PixelRemapper> DrawTarget
733    for RemappedFrameBuffer<F, M>
734{
735    type Color = Color;
736    type Error = Infallible;
737
738    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
739    where
740        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
741    {
742        self.0.draw_iter(pixels.into_iter().map(M::remap))
743    }
744}
745
746impl<F: DrawTarget<Error = Infallible, Color = Color>, M: PixelRemapper> OriginDimensions
747    for RemappedFrameBuffer<F, M>
748{
749    fn size(&self) -> Size {
750        Size::new(M::virtual_size().1 as u32, M::virtual_size().0 as u32)
751    }
752}
753
754impl<F: FrameBufferOperations + FrameBuffer, M: PixelRemapper> FrameBufferOperations
755    for RemappedFrameBuffer<F, M>
756{
757    #[inline]
758    fn erase(&mut self) {
759        self.0.erase();
760    }
761
762    #[inline]
763    fn set_pixel(&mut self, p: Point, color: Color) {
764        self.0.set_pixel(M::remap_point(p), color);
765    }
766}
767
768impl<F: FrameBuffer, M: PixelRemapper> FrameBuffer for RemappedFrameBuffer<F, M> {
769    type Word = F::Word;
770
771    const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY] = F::BCM_SEQUENCE;
772
773    const BCM_SEQUENCE_LEN: usize = F::BCM_SEQUENCE_LEN;
774
775    const BCM_SEQUENCE_COUNT: usize = F::BCM_SEQUENCE_COUNT;
776
777    const BCM_SEGMENTS_PER_GROUP: usize = F::BCM_SEGMENTS_PER_GROUP;
778
779    fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
780        self.0.bcm_segment_ptr(index)
781    }
782}
783
784impl<F: MutableFrameBuffer, M: PixelRemapper> MutableFrameBuffer for RemappedFrameBuffer<F, M> {}
785
786#[cfg(test)]
787mod tests {
788    extern crate std;
789
790    use embedded_graphics::prelude::*;
791
792    use super::*;
793    use crate::MutableFrameBuffer;
794    use core::convert::Infallible;
795
796    #[test]
797    fn test_virtual_size_function_with_equal_rows_and_cols() {
798        const ROWS_IN_PANEL: usize = 32;
799        const COLS_IN_PANEL: usize = 64;
800        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 3>;
801        let virt_size = PanelChain::virtual_size();
802        assert_eq!(virt_size, (ROWS_IN_PANEL * 3, COLS_IN_PANEL * 3));
803    }
804
805    #[test]
806    fn test_virtual_size_function_with_uneven_rows_and_cols() {
807        const ROWS_IN_PANEL: usize = 32;
808        const COLS_IN_PANEL: usize = 64;
809        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 5, 3>;
810        let virt_size = PanelChain::virtual_size();
811        assert_eq!(virt_size, (ROWS_IN_PANEL * 5, COLS_IN_PANEL * 3));
812    }
813
814    #[test]
815    fn test_virtual_size_function_with_single_column() {
816        const ROWS_IN_PANEL: usize = 32;
817        const COLS_IN_PANEL: usize = 64;
818        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 1>;
819        let virt_size = PanelChain::virtual_size();
820        assert_eq!(virt_size, (ROWS_IN_PANEL * 3, COLS_IN_PANEL));
821    }
822
823    #[test]
824    fn test_fb_size_function_with_equal_rows_and_cols() {
825        const ROWS_IN_PANEL: usize = 32;
826        const COLS_IN_PANEL: usize = 64;
827        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 3>;
828        let virt_size = PanelChain::fb_size();
829        assert_eq!(virt_size, (ROWS_IN_PANEL, COLS_IN_PANEL * 9));
830    }
831
832    #[test]
833    fn test_fb_size_function_with_uneven_rows_and_cols() {
834        const ROWS_IN_PANEL: usize = 32;
835        const COLS_IN_PANEL: usize = 64;
836        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 5, 3>;
837        let virt_size = PanelChain::fb_size();
838        assert_eq!(virt_size, (ROWS_IN_PANEL, COLS_IN_PANEL * 15));
839    }
840
841    #[test]
842    fn test_fb_size_function_with_single_column() {
843        const ROWS_IN_PANEL: usize = 32;
844        const COLS_IN_PANEL: usize = 64;
845        type PanelChain = ChainTopRightDown<ROWS_IN_PANEL, COLS_IN_PANEL, 3, 1>;
846        let virt_size = PanelChain::fb_size();
847        assert_eq!(virt_size, (ROWS_IN_PANEL, COLS_IN_PANEL * 3));
848    }
849
850    #[test]
851    fn test_pixel_remap_top_right_down_point_in_origin() {
852        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
853
854        let pixel = PanelChain::remap(Pixel(Point::new(0, 0), Color::RED));
855        assert_eq!(pixel.0, Point::new(384, 0));
856    }
857
858    #[test]
859    fn test_pixel_remap_top_right_down_point_in_bottom_left_corner() {
860        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
861
862        let pixel = PanelChain::remap(Pixel(Point::new(0, 95), Color::RED));
863        assert_eq!(pixel.0, Point::new(0, 31));
864    }
865
866    #[test]
867    fn test_pixel_remap_top_right_down_point_in_bottom_right_corner() {
868        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
869
870        let pixel = PanelChain::remap(Pixel(Point::new(191, 95), Color::RED));
871        assert_eq!(pixel.0, Point::new(191, 31));
872    }
873
874    #[test]
875    fn test_pixel_remap_top_right_down_point_on_x_right_edge_of_first_panel() {
876        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
877
878        let pixel = PanelChain::remap(Pixel(Point::new(63, 0), Color::RED));
879        assert_eq!(pixel.0, Point::new(447, 0));
880    }
881
882    #[test]
883    fn test_pixel_remap_top_right_down_point_on_x_left_edge_of_second_panel() {
884        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
885
886        let pixel = PanelChain::remap(Pixel(Point::new(64, 0), Color::RED));
887        assert_eq!(pixel.0, Point::new(448, 0));
888    }
889
890    #[test]
891    fn test_pixel_remap_top_right_down_point_on_y_bottom_edge_of_first_panel() {
892        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
893
894        let pixel = PanelChain::remap(Pixel(Point::new(0, 31), Color::RED));
895        assert_eq!(pixel.0, Point::new(384, 31));
896    }
897
898    #[test]
899    fn test_pixel_remap_top_right_down_point_on_y_top_edge_of_fourth_panel() {
900        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
901
902        let pixel = PanelChain::remap(Pixel(Point::new(0, 32), Color::RED));
903        assert_eq!(pixel.0, Point::new(383, 31));
904    }
905
906    #[test]
907    fn test_pixel_remap_top_right_down_point_slightly_to_the_top_middle() {
908        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
909
910        let pixel = PanelChain::remap(Pixel(Point::new(100, 40), Color::RED));
911        assert_eq!(pixel.0, Point::new(283, 23));
912    }
913
914    #[test]
915    fn test_pixel_remap_negative_pixel_does_not_remap() {
916        type PanelChain = ChainTopRightDown<32, 64, 3, 3>;
917
918        let pixel = PanelChain::remap(Pixel(Point::new(-5, 40), Color::RED));
919        assert_eq!(pixel.0, Point::new(-5, 40));
920    }
921
922    #[test]
923    fn test_compute_tiled_cols() {
924        assert_eq!(192, compute_tiled_cols(32, 3, 2));
925    }
926
927    #[test]
928    #[allow(deprecated)]
929    fn test_tiling_framebuffer_canvas_size() {
930        use crate::plain::DmaFrameBuffer;
931        use crate::tiling::{compute_tiled_cols, ChainTopRightDown, TiledFrameBuffer};
932        use crate::{compute_frame_count, compute_rows};
933
934        const TILED_COLS: usize = 3;
935        const TILED_ROWS: usize = 3;
936        const ROWS: usize = 32;
937        const PANEL_COLS: usize = 64;
938        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
939        const BITS: u8 = 2;
940        const NROWS: usize = compute_rows(ROWS);
941        const FRAME_COUNT: usize = compute_frame_count(BITS);
942
943        type FBType = DmaFrameBuffer<ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>;
944        type TiledFBType = TiledFrameBuffer<
945            FBType,
946            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
947            ROWS,
948            PANEL_COLS,
949            NROWS,
950            BITS,
951            FRAME_COUNT,
952            TILED_ROWS,
953            TILED_COLS,
954            FB_COLS,
955        >;
956
957        let fb = TiledFBType::new();
958
959        assert_eq!(fb.size(), Size::new(192, 96));
960    }
961
962    // Test helper framebuffer that records calls for verification
963    struct TestFrameBuffer {
964        calls: std::cell::RefCell<std::vec::Vec<Call>>,
965        buf: [u8; 8],
966    }
967
968    #[derive(Debug, Clone, PartialEq, Eq)]
969    enum Call {
970        Erase,
971        SetPixel { p: Point, color: Color },
972        Draw(std::vec::Vec<(Point, Color)>),
973    }
974
975    impl TestFrameBuffer {
976        fn new() -> Self {
977            Self {
978                calls: std::cell::RefCell::new(std::vec::Vec::new()),
979                buf: [0; 8],
980            }
981        }
982
983        fn take_calls(&self) -> std::vec::Vec<Call> {
984            core::mem::take(&mut *self.calls.borrow_mut())
985        }
986    }
987
988    impl Default for TestFrameBuffer {
989        fn default() -> Self {
990            Self::new()
991        }
992    }
993
994    impl DrawTarget for TestFrameBuffer {
995        type Color = Color;
996        type Error = Infallible;
997
998        fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
999        where
1000            I: IntoIterator<Item = Pixel<Self::Color>>,
1001        {
1002            let v = pixels.into_iter().map(|p| (p.0, p.1)).collect();
1003            self.calls.borrow_mut().push(Call::Draw(v));
1004            Ok(())
1005        }
1006    }
1007
1008    impl OriginDimensions for TestFrameBuffer {
1009        fn size(&self) -> Size {
1010            Size::new(1, 1)
1011        }
1012    }
1013
1014    impl FrameBuffer for TestFrameBuffer {
1015        type Word = u8;
1016
1017        const BCM_SEQUENCE: [crate::BcmLenReps; crate::BCM_SEQUENCE_CAPACITY] = {
1018            let mut seq = [crate::BcmLenReps::ZERO; crate::BCM_SEQUENCE_CAPACITY];
1019            seq[0] = crate::BcmLenReps { len: 8, reps: 1 };
1020            seq
1021        };
1022
1023        const BCM_SEQUENCE_LEN: usize = 1;
1024
1025        const BCM_SEQUENCE_COUNT: usize = 1;
1026
1027        fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
1028            assert!(index == 0);
1029            self.buf.as_ptr()
1030        }
1031    }
1032
1033    impl FrameBufferOperations for TestFrameBuffer {
1034        fn erase(&mut self) {
1035            self.calls.borrow_mut().push(Call::Erase);
1036        }
1037
1038        fn set_pixel(&mut self, p: Point, color: Color) {
1039            self.calls.borrow_mut().push(Call::SetPixel { p, color });
1040        }
1041    }
1042
1043    impl MutableFrameBuffer for TestFrameBuffer {}
1044
1045    #[test]
1046    #[allow(deprecated)]
1047    fn test_tiled_draw_iter_forwards_with_remap() {
1048        const TILED_COLS: usize = 3;
1049        const TILED_ROWS: usize = 3;
1050        const ROWS: usize = 32;
1051        const PANEL_COLS: usize = 64;
1052        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1053
1054        let mut fb = TiledFrameBuffer::<
1055            TestFrameBuffer,
1056            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1057            ROWS,
1058            PANEL_COLS,
1059            { crate::compute_rows(ROWS) },
1060            2,
1061            { crate::compute_frame_count(2) },
1062            TILED_ROWS,
1063            TILED_COLS,
1064            FB_COLS,
1065        >(TestFrameBuffer::new(), core::marker::PhantomData);
1066
1067        let input = [
1068            Pixel(Point::new(0, 0), Color::RED),
1069            Pixel(Point::new(63, 0), Color::GREEN),
1070            Pixel(Point::new(64, 0), Color::BLUE),
1071            Pixel(Point::new(100, 40), Color::WHITE),
1072        ];
1073
1074        fb.draw_iter(input.into_iter()).unwrap();
1075
1076        let calls = fb.0.take_calls();
1077        assert_eq!(calls.len(), 1);
1078        match &calls[0] {
1079            Call::Draw(v) => {
1080                let expected =
1081                    [
1082                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
1083                            Pixel(Point::new(0, 0), Color::RED),
1084                        ),
1085                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
1086                            Pixel(Point::new(63, 0), Color::GREEN),
1087                        ),
1088                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
1089                            Pixel(Point::new(64, 0), Color::BLUE),
1090                        ),
1091                        ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap(
1092                            Pixel(Point::new(100, 40), Color::WHITE),
1093                        ),
1094                    ];
1095                let expected_points: std::vec::Vec<(Point, Color)> =
1096                    expected.iter().map(|p| (p.0, p.1)).collect();
1097                assert_eq!(v.as_slice(), expected_points.as_slice());
1098            }
1099            _ => panic!("expected a Draw call"),
1100        }
1101    }
1102
1103    #[test]
1104    #[allow(deprecated)]
1105    fn test_tiled_set_pixel_remaps_and_forwards() {
1106        const TILED_COLS: usize = 3;
1107        const TILED_ROWS: usize = 3;
1108        const ROWS: usize = 32;
1109        const PANEL_COLS: usize = 64;
1110        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1111
1112        let mut fb = TiledFrameBuffer::<
1113            TestFrameBuffer,
1114            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1115            ROWS,
1116            PANEL_COLS,
1117            { crate::compute_rows(ROWS) },
1118            2,
1119            { crate::compute_frame_count(2) },
1120            TILED_ROWS,
1121            TILED_COLS,
1122            FB_COLS,
1123        >(TestFrameBuffer::new(), core::marker::PhantomData);
1124
1125        let p = Point::new(100, 40);
1126        fb.set_pixel(p, Color::BLUE);
1127
1128        let calls = fb.0.take_calls();
1129        assert_eq!(calls.len(), 1);
1130        match calls.into_iter().next().unwrap() {
1131            Call::SetPixel { p: rp, color } => {
1132                let expected =
1133                    ChainTopRightDown::<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>::remap_point(p);
1134                assert_eq!(rp, expected);
1135                assert_eq!(color, Color::BLUE);
1136            }
1137            _ => panic!("expected a SetPixel call"),
1138        }
1139    }
1140
1141    #[test]
1142    #[allow(deprecated)]
1143    fn test_tiled_erase_forwards() {
1144        const TILED_COLS: usize = 2;
1145        const TILED_ROWS: usize = 2;
1146        const ROWS: usize = 32;
1147        const PANEL_COLS: usize = 64;
1148        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1149
1150        let mut fb = TiledFrameBuffer::<
1151            TestFrameBuffer,
1152            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1153            ROWS,
1154            PANEL_COLS,
1155            { crate::compute_rows(ROWS) },
1156            2,
1157            { crate::compute_frame_count(2) },
1158            TILED_ROWS,
1159            TILED_COLS,
1160            FB_COLS,
1161        >(TestFrameBuffer::new(), core::marker::PhantomData);
1162        fb.erase();
1163        let calls = fb.0.take_calls();
1164        assert_eq!(calls, std::vec![Call::Erase]);
1165    }
1166
1167    #[test]
1168    #[allow(deprecated)]
1169    fn test_tiled_negative_coordinates_not_remapped() {
1170        const TILED_COLS: usize = 2;
1171        const TILED_ROWS: usize = 2;
1172        const ROWS: usize = 32;
1173        const PANEL_COLS: usize = 64;
1174        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1175
1176        let mut fb = TiledFrameBuffer::<
1177            TestFrameBuffer,
1178            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1179            ROWS,
1180            PANEL_COLS,
1181            { crate::compute_rows(ROWS) },
1182            2,
1183            { crate::compute_frame_count(2) },
1184            TILED_ROWS,
1185            TILED_COLS,
1186            FB_COLS,
1187        >(TestFrameBuffer::new(), core::marker::PhantomData);
1188
1189        // set_pixel path
1190        let neg = Point::new(-3, 5);
1191        fb.set_pixel(neg, Color::GREEN);
1192        // draw_iter path
1193        fb.draw_iter(core::iter::once(Pixel(Point::new(10, -2), Color::RED)))
1194            .unwrap();
1195
1196        let calls = fb.0.take_calls();
1197        assert_eq!(calls.len(), 2);
1198        assert!(matches!(calls[0], Call::SetPixel { p, .. } if p == neg));
1199        match &calls[1] {
1200            Call::Draw(v) => {
1201                assert_eq!(v.as_slice(), &[(Point::new(10, -2), Color::RED)]);
1202            }
1203            _ => panic!("expected a Draw call"),
1204        }
1205    }
1206
1207    // Remapper that generates very large coordinates to trigger u16 truncation in remap_point
1208    struct Huge<
1209        const PANEL_ROWS: usize,
1210        const PANEL_COLS: usize,
1211        const TILE_ROWS: usize,
1212        const TILE_COLS: usize,
1213    >;
1214
1215    impl<
1216            const PANEL_ROWS: usize,
1217            const PANEL_COLS: usize,
1218            const TILE_ROWS: usize,
1219            const TILE_COLS: usize,
1220        > PixelRemapper for Huge<PANEL_ROWS, PANEL_COLS, TILE_ROWS, TILE_COLS>
1221    {
1222        const VIRT_ROWS: usize = PANEL_ROWS * TILE_ROWS;
1223        const VIRT_COLS: usize = PANEL_COLS * TILE_COLS;
1224        const FB_ROWS: usize = PANEL_ROWS;
1225        const FB_COLS: usize = PANEL_COLS * TILE_ROWS * TILE_COLS;
1226
1227        fn remap_xy(x: usize, y: usize) -> (usize, usize) {
1228            (x + 70_000, y + 70_000)
1229        }
1230    }
1231
1232    #[test]
1233    #[allow(deprecated)]
1234    fn test_remap_point_truncates_to_u16_range() {
1235        const TILED_COLS: usize = 1;
1236        const TILED_ROWS: usize = 1;
1237        const ROWS: usize = 32;
1238        const PANEL_COLS: usize = 64;
1239        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1240
1241        let mut fb = TiledFrameBuffer::<
1242            TestFrameBuffer,
1243            Huge<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1244            ROWS,
1245            PANEL_COLS,
1246            { crate::compute_rows(ROWS) },
1247            2,
1248            { crate::compute_frame_count(2) },
1249            TILED_ROWS,
1250            TILED_COLS,
1251            FB_COLS,
1252        >(TestFrameBuffer::new(), core::marker::PhantomData);
1253        fb.set_pixel(Point::new(1, 2), Color::RED);
1254
1255        let calls = fb.0.take_calls();
1256        match calls.into_iter().next().unwrap() {
1257            Call::SetPixel { p, color } => {
1258                let (rx, ry) = (1usize + 70_000, 2usize + 70_000);
1259                let expected = Point::new(i32::from(rx as u16), i32::from(ry as u16));
1260                assert_eq!(p, expected);
1261                assert_eq!(color, Color::RED);
1262            }
1263            other => panic!("unexpected call recorded: {other:?}"),
1264        }
1265    }
1266
1267    #[test]
1268    fn test_more_compute_tiled_cols_cases() {
1269        assert_eq!(compute_tiled_cols(64, 1, 4), 256);
1270        assert_eq!(compute_tiled_cols(64, 4, 1), 256);
1271        assert_eq!(compute_tiled_cols(32, 4, 5), 640);
1272    }
1273
1274    #[test]
1275    #[allow(deprecated)]
1276    fn test_tiled_default_and_new_construct() {
1277        const TILED_COLS: usize = 4;
1278        const TILED_ROWS: usize = 2;
1279        const ROWS: usize = 32;
1280        const PANEL_COLS: usize = 64;
1281        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1282
1283        let fb_default = TiledFrameBuffer::<
1284            TestFrameBuffer,
1285            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1286            ROWS,
1287            PANEL_COLS,
1288            { crate::compute_rows(ROWS) },
1289            2,
1290            { crate::compute_frame_count(2) },
1291            TILED_ROWS,
1292            TILED_COLS,
1293            FB_COLS,
1294        >::default();
1295
1296        let fb_new = TiledFrameBuffer::<
1297            TestFrameBuffer,
1298            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1299            ROWS,
1300            PANEL_COLS,
1301            { crate::compute_rows(ROWS) },
1302            2,
1303            { crate::compute_frame_count(2) },
1304            TILED_ROWS,
1305            TILED_COLS,
1306            FB_COLS,
1307        >::new();
1308
1309        // Size comes from OriginDimensions impl on TiledFrameBuffer (via M::virtual_size)
1310        let expected_size = Size::new((PANEL_COLS * TILED_COLS) as u32, (ROWS * TILED_ROWS) as u32);
1311        assert_eq!(fb_default.size(), expected_size);
1312        assert_eq!(fb_new.size(), expected_size);
1313
1314        // No calls recorded yet on inner framebuffer
1315        assert!(fb_default.0.take_calls().is_empty());
1316        assert!(fb_new.0.take_calls().is_empty());
1317    }
1318
1319    #[test]
1320    #[allow(deprecated)]
1321    fn test_tiled_origin_dimensions_matches_virtual_size() {
1322        const TILED_COLS: usize = 5;
1323        const TILED_ROWS: usize = 2;
1324        const ROWS: usize = 32;
1325        const PANEL_COLS: usize = 64;
1326        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1327
1328        let fb = TiledFrameBuffer::<
1329            TestFrameBuffer,
1330            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1331            ROWS,
1332            PANEL_COLS,
1333            { crate::compute_rows(ROWS) },
1334            2,
1335            { crate::compute_frame_count(2) },
1336            TILED_ROWS,
1337            TILED_COLS,
1338            FB_COLS,
1339        >::new();
1340
1341        let (virt_rows, virt_cols) = <ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS> as PixelRemapper>::virtual_size();
1342        assert_eq!(fb.size(), Size::new(virt_cols as u32, virt_rows as u32));
1343    }
1344
1345    // Expected mapping for ChainTopRightDown (current, correct behavior)
1346    fn expected_ctrdd_xy<const PR: usize, const PC: usize, const TR: usize, const TC: usize>(
1347        x: usize,
1348        y: usize,
1349    ) -> (usize, usize) {
1350        let row = y / PR;
1351        let base = (TR - 1 - row) * (PC * TC);
1352        if row % 2 == 1 {
1353            (base + (PC * TC) - 1 - x, PR - 1 - (y % PR))
1354        } else {
1355            (base + x, y % PR)
1356        }
1357    }
1358
1359    #[test]
1360    fn test_chain_top_right_down_corners_2x3() {
1361        const PR: usize = 32;
1362        const PC: usize = 64;
1363        const TR: usize = 2;
1364        const TC: usize = 3;
1365        type M = ChainTopRightDown<PR, PC, TR, TC>;
1366
1367        for r in 0..TR {
1368            for c in 0..TC {
1369                let x0 = c * PC;
1370                let y0 = r * PR;
1371                let corners = [
1372                    (x0, y0),                   // TL
1373                    (x0 + PC - 1, y0),          // TR
1374                    (x0, y0 + PR - 1),          // BL
1375                    (x0 + PC - 1, y0 + PR - 1), // BR
1376                ];
1377
1378                for &(x, y) in &corners {
1379                    let got = <M as PixelRemapper>::remap_xy(x, y);
1380                    let exp = expected_ctrdd_xy::<PR, PC, TR, TC>(x, y);
1381                    assert_eq!(
1382                        got, exp,
1383                        "corner mismatch at panel (row={}, col={}), virtual=({}, {})",
1384                        r, c, x, y
1385                    );
1386                }
1387            }
1388        }
1389    }
1390
1391    #[test]
1392    fn test_chain_top_right_down_corners_3x2() {
1393        const PR: usize = 32;
1394        const PC: usize = 64;
1395        const TR: usize = 3;
1396        const TC: usize = 2;
1397        type M = ChainTopRightDown<PR, PC, TR, TC>;
1398
1399        for r in 0..TR {
1400            for c in 0..TC {
1401                let x0 = c * PC;
1402                let y0 = r * PR;
1403                let corners = [
1404                    (x0, y0),                   // TL
1405                    (x0 + PC - 1, y0),          // TR
1406                    (x0, y0 + PR - 1),          // BL
1407                    (x0 + PC - 1, y0 + PR - 1), // BR
1408                ];
1409
1410                for &(x, y) in &corners {
1411                    let got = <M as PixelRemapper>::remap_xy(x, y);
1412                    let exp = expected_ctrdd_xy::<PR, PC, TR, TC>(x, y);
1413                    assert_eq!(
1414                        got, exp,
1415                        "corner mismatch at panel (row={}, col={}), virtual=({}, {})",
1416                        r, c, x, y
1417                    );
1418                }
1419            }
1420        }
1421    }
1422
1423    // ---- RemappedFrameBuffer tests ----
1424
1425    type TestRemapped = RemappedFrameBuffer<TestFrameBuffer, ChainTopRightDown<32, 64, 3, 3>>;
1426
1427    #[test]
1428    fn test_remapped_default_and_new_construct() {
1429        let _fb = TestRemapped::new();
1430        let _fb2: TestRemapped = Default::default();
1431    }
1432
1433    #[test]
1434    fn test_remapped_origin_dimensions_matches_virtual_size() {
1435        let fb = TestRemapped::new();
1436        assert_eq!(fb.size(), Size::new(192, 96));
1437    }
1438
1439    #[test]
1440    fn test_remapped_draw_iter_forwards_with_remap() {
1441        let mut fb = RemappedFrameBuffer::<TestFrameBuffer, ChainTopRightDown<32, 64, 3, 3>>(
1442            TestFrameBuffer::new(),
1443            core::marker::PhantomData,
1444        );
1445
1446        let input = [
1447            Pixel(Point::new(0, 0), Color::RED),
1448            Pixel(Point::new(63, 0), Color::GREEN),
1449            Pixel(Point::new(64, 0), Color::BLUE),
1450            Pixel(Point::new(100, 40), Color::WHITE),
1451        ];
1452
1453        fb.draw_iter(input.into_iter()).unwrap();
1454
1455        let calls = fb.0.take_calls();
1456        assert_eq!(calls.len(), 1);
1457        match &calls[0] {
1458            Call::Draw(v) => {
1459                let expected = [
1460                    ChainTopRightDown::<32, 64, 3, 3>::remap(Pixel(Point::new(0, 0), Color::RED)),
1461                    ChainTopRightDown::<32, 64, 3, 3>::remap(Pixel(
1462                        Point::new(63, 0),
1463                        Color::GREEN,
1464                    )),
1465                    ChainTopRightDown::<32, 64, 3, 3>::remap(Pixel(Point::new(64, 0), Color::BLUE)),
1466                    ChainTopRightDown::<32, 64, 3, 3>::remap(Pixel(
1467                        Point::new(100, 40),
1468                        Color::WHITE,
1469                    )),
1470                ];
1471                let expected_points: std::vec::Vec<(Point, Color)> =
1472                    expected.iter().map(|p| (p.0, p.1)).collect();
1473                assert_eq!(v.as_slice(), expected_points.as_slice());
1474            }
1475            _ => panic!("expected a Draw call"),
1476        }
1477    }
1478
1479    #[test]
1480    fn test_remapped_set_pixel_remaps_and_forwards() {
1481        let mut fb = RemappedFrameBuffer::<TestFrameBuffer, ChainTopRightDown<32, 64, 3, 3>>(
1482            TestFrameBuffer::new(),
1483            core::marker::PhantomData,
1484        );
1485
1486        let p = Point::new(100, 40);
1487        fb.set_pixel(p, Color::BLUE);
1488
1489        let calls = fb.0.take_calls();
1490        assert_eq!(calls.len(), 1);
1491        match calls.into_iter().next().unwrap() {
1492            Call::SetPixel { p: rp, color } => {
1493                let expected = ChainTopRightDown::<32, 64, 3, 3>::remap_point(p);
1494                assert_eq!(rp, expected);
1495                assert_eq!(color, Color::BLUE);
1496            }
1497            _ => panic!("expected a SetPixel call"),
1498        }
1499    }
1500
1501    #[test]
1502    fn test_remapped_erase_forwards() {
1503        let mut fb = RemappedFrameBuffer::<TestFrameBuffer, ChainTopRightDown<32, 64, 2, 2>>(
1504            TestFrameBuffer::new(),
1505            core::marker::PhantomData,
1506        );
1507
1508        fb.erase();
1509        let calls = fb.0.take_calls();
1510        assert_eq!(calls, std::vec![Call::Erase]);
1511    }
1512
1513    #[test]
1514    fn test_remapped_negative_coordinates_not_remapped() {
1515        let mut fb = RemappedFrameBuffer::<TestFrameBuffer, ChainTopRightDown<32, 64, 2, 2>>(
1516            TestFrameBuffer::new(),
1517            core::marker::PhantomData,
1518        );
1519
1520        let neg = Point::new(-3, 5);
1521        fb.set_pixel(neg, Color::GREEN);
1522        fb.draw_iter(core::iter::once(Pixel(Point::new(10, -2), Color::RED)))
1523            .unwrap();
1524
1525        let calls = fb.0.take_calls();
1526        assert_eq!(calls.len(), 2);
1527        assert!(matches!(calls[0], Call::SetPixel { p, .. } if p == neg));
1528        match &calls[1] {
1529            Call::Draw(v) => {
1530                assert_eq!(v.as_slice(), &[(Point::new(10, -2), Color::RED)]);
1531            }
1532            _ => panic!("expected a Draw call"),
1533        }
1534    }
1535
1536    #[test]
1537    fn test_remapped_with_plain_framebuffer() {
1538        use crate::plain::DmaFrameBuffer;
1539        const TILED_COLS: usize = 3;
1540        const TILED_ROWS: usize = 3;
1541        const ROWS: usize = 32;
1542        const PANEL_COLS: usize = 64;
1543        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1544        const BITS: u8 = 2;
1545        const NROWS: usize = crate::compute_rows(ROWS);
1546        const FRAME_COUNT: usize = crate::compute_frame_count(BITS);
1547
1548        type FBType = DmaFrameBuffer<ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>;
1549        type Display = RemappedFrameBuffer<
1550            FBType,
1551            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1552        >;
1553
1554        let fb = Display::new();
1555        assert_eq!(fb.size(), Size::new(192, 96));
1556    }
1557
1558    #[test]
1559    fn test_remapped_with_latched_framebuffer() {
1560        use crate::latched::DmaFrameBuffer;
1561        const TILED_COLS: usize = 3;
1562        const TILED_ROWS: usize = 3;
1563        const ROWS: usize = 32;
1564        const PANEL_COLS: usize = 64;
1565        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1566        const BITS: u8 = 2;
1567        const NROWS: usize = crate::compute_rows(ROWS);
1568        const FRAME_COUNT: usize = crate::compute_frame_count(BITS);
1569
1570        type FBType = DmaFrameBuffer<ROWS, FB_COLS, NROWS, BITS, FRAME_COUNT>;
1571        type Display = RemappedFrameBuffer<
1572            FBType,
1573            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1574        >;
1575
1576        let fb = Display::new();
1577        assert_eq!(fb.size(), Size::new(192, 96));
1578    }
1579
1580    #[test]
1581    fn test_remapped_with_bitplane_plain_framebuffer() {
1582        use crate::bitplane::plain::DmaFrameBuffer;
1583        const TILED_COLS: usize = 3;
1584        const TILED_ROWS: usize = 3;
1585        const ROWS: usize = 32;
1586        const NROWS: usize = ROWS / 2;
1587        const PANEL_COLS: usize = 64;
1588        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1589        const PLANES: usize = 8;
1590
1591        type FBType = DmaFrameBuffer<NROWS, FB_COLS, PLANES>;
1592        type Display = RemappedFrameBuffer<
1593            FBType,
1594            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1595        >;
1596
1597        let fb = Display::new();
1598        assert_eq!(fb.size(), Size::new(192, 96));
1599    }
1600
1601    #[test]
1602    fn test_remapped_with_bitplane_latched_framebuffer() {
1603        use crate::bitplane::latched::DmaFrameBuffer;
1604        const TILED_COLS: usize = 3;
1605        const TILED_ROWS: usize = 3;
1606        const ROWS: usize = 32;
1607        const NROWS: usize = ROWS / 2;
1608        const PANEL_COLS: usize = 64;
1609        const FB_COLS: usize = compute_tiled_cols(PANEL_COLS, TILED_ROWS, TILED_COLS);
1610        const PLANES: usize = 8;
1611
1612        type FBType = DmaFrameBuffer<NROWS, FB_COLS, PLANES>;
1613        type Display = RemappedFrameBuffer<
1614            FBType,
1615            ChainTopRightDown<ROWS, PANEL_COLS, TILED_ROWS, TILED_COLS>,
1616        >;
1617
1618        let fb = Display::new();
1619        assert_eq!(fb.size(), Size::new(192, 96));
1620    }
1621
1622    #[test]
1623    fn test_quarter_scan_associated_constants() {
1624        type QS = QuarterScan<64, 64>;
1625        assert_eq!(QS::VIRT_ROWS, 64);
1626        assert_eq!(QS::VIRT_COLS, 64);
1627        assert_eq!(QS::FB_ROWS, 32);
1628        assert_eq!(QS::FB_COLS, 128);
1629        assert_eq!(QS::virtual_size(), (64, 64));
1630        assert_eq!(QS::fb_size(), (32, 128));
1631    }
1632
1633    #[test]
1634    fn test_quarter_scan_remap_xy_groups() {
1635        type QS = QuarterScan<64, 64>;
1636        // group 0 (virtual rows 0..16) → channel 1, section 1
1637        assert_eq!(QS::remap_xy(0, 0), (64, 0));
1638        assert_eq!(QS::remap_xy(63, 15), (127, 15));
1639        // group 1 (virtual rows 16..32) → channel 1, section 0
1640        assert_eq!(QS::remap_xy(0, 16), (0, 0));
1641        assert_eq!(QS::remap_xy(63, 31), (63, 15));
1642        // group 2 (virtual rows 32..48) → channel 2, section 1
1643        assert_eq!(QS::remap_xy(0, 32), (64, 16));
1644        assert_eq!(QS::remap_xy(63, 47), (127, 31));
1645        // group 3 (virtual rows 48..64) → channel 2, section 0
1646        assert_eq!(QS::remap_xy(0, 48), (0, 16));
1647        assert_eq!(QS::remap_xy(63, 63), (63, 31));
1648    }
1649
1650    #[test]
1651    fn test_quarter_scan_remap_out_of_bounds_is_clipped() {
1652        type QS = QuarterScan<64, 64>;
1653        // points outside the 64×64 virtual panel map just past the inner
1654        // framebuffer bounds (128×32) so the inner framebuffer discards them
1655        assert_eq!(QS::remap_xy(64, 0), (128, 32));
1656        assert_eq!(QS::remap_xy(0, 64), (128, 32));
1657        assert_eq!(QS::remap_xy(100, 100), (128, 32));
1658        // the last valid pixel is unaffected
1659        assert_eq!(QS::remap_xy(63, 63), (63, 31));
1660    }
1661
1662    #[test]
1663    fn test_quarter_scan_offscreen_pixels_do_not_panic() {
1664        // embedded-graphics generates pixels outside the canvas when drawing
1665        // text with its baseline on the last row (6×8 cell extending below
1666        // the baseline) — these must be clipped, not panic.
1667        use crate::bitplane::plain::DmaFrameBuffer;
1668        type InnerFB = DmaFrameBuffer<16, 128, 4>;
1669        type Display = RemappedFrameBuffer<InnerFB, QuarterScan<64, 64>>;
1670
1671        let mut fb = Display::new();
1672        for x in 0..70 {
1673            for y in 57..70 {
1674                fb.set_pixel(Point::new(x, y), Color::WHITE);
1675            }
1676        }
1677    }
1678
1679    #[test]
1680    fn test_quarter_scan_remap_is_bijective_and_in_bounds() {
1681        use super::quarter_scan::*;
1682        fn check<V: Variant>() {
1683            let mut seen = [false; 32 * 128];
1684            for y in 0..QuarterScan::<64, 64, V>::VIRT_ROWS {
1685                for x in 0..QuarterScan::<64, 64, V>::VIRT_COLS {
1686                    let (fb_x, fb_y) = QuarterScan::<64, 64, V>::remap_xy(x, y);
1687                    assert!(
1688                        fb_x < QuarterScan::<64, 64, V>::FB_COLS,
1689                        "fb_x {fb_x} out of bounds"
1690                    );
1691                    assert!(
1692                        fb_y < QuarterScan::<64, 64, V>::FB_ROWS,
1693                        "fb_y {fb_y} out of bounds"
1694                    );
1695                    let idx = fb_y * QuarterScan::<64, 64, V>::FB_COLS + fb_x;
1696                    assert!(!seen[idx], "duplicate mapping to ({fb_x}, {fb_y})");
1697                    seen[idx] = true;
1698                }
1699            }
1700            // every framebuffer cell is covered exactly once
1701            assert!(seen.iter().all(|&s| s));
1702        }
1703        check::<Linear>();
1704        check::<SectionsSwapped>();
1705        check::<HalvesSwapped>();
1706        check::<Alternating>();
1707    }
1708
1709    #[test]
1710    fn test_quarter_scan_variant_tables_are_permutations() {
1711        use super::quarter_scan::*;
1712        fn assert_permutation<V: Variant>() {
1713            let mut seen = [false; 4];
1714            for &(channel, section) in V::SLOT.iter() {
1715                assert!(channel < 2 && section < 2, "slot out of range");
1716                let idx = channel * 2 + section;
1717                assert!(!seen[idx], "duplicate slot in variant table");
1718                seen[idx] = true;
1719            }
1720            assert!(
1721                seen.iter().all(|&s| s),
1722                "variant table must cover all four slots"
1723            );
1724        }
1725        assert_permutation::<Linear>();
1726        assert_permutation::<SectionsSwapped>();
1727        assert_permutation::<HalvesSwapped>();
1728        assert_permutation::<Alternating>();
1729    }
1730
1731    #[test]
1732    fn test_quarter_scan_variant_mappings() {
1733        use super::quarter_scan::*;
1734        // Linear: naive row order
1735        assert_eq!(QuarterScan::<64, 64, Linear>::remap_xy(0, 0), (0, 0));
1736        assert_eq!(QuarterScan::<64, 64, Linear>::remap_xy(0, 16), (64, 0));
1737        assert_eq!(QuarterScan::<64, 64, Linear>::remap_xy(0, 32), (0, 16));
1738        assert_eq!(QuarterScan::<64, 64, Linear>::remap_xy(0, 48), (64, 16));
1739        // HalvesSwapped: top and bottom halves exchanged
1740        assert_eq!(
1741            QuarterScan::<64, 64, HalvesSwapped>::remap_xy(0, 0),
1742            (0, 16)
1743        );
1744        assert_eq!(
1745            QuarterScan::<64, 64, HalvesSwapped>::remap_xy(0, 16),
1746            (64, 16)
1747        );
1748        assert_eq!(
1749            QuarterScan::<64, 64, HalvesSwapped>::remap_xy(0, 32),
1750            (0, 0)
1751        );
1752        assert_eq!(
1753            QuarterScan::<64, 64, HalvesSwapped>::remap_xy(0, 48),
1754            (64, 0)
1755        );
1756        // Alternating: channel-interleaved groups
1757        assert_eq!(QuarterScan::<64, 64, Alternating>::remap_xy(0, 0), (0, 0));
1758        assert_eq!(QuarterScan::<64, 64, Alternating>::remap_xy(0, 16), (0, 16));
1759        assert_eq!(QuarterScan::<64, 64, Alternating>::remap_xy(0, 32), (64, 0));
1760        assert_eq!(
1761            QuarterScan::<64, 64, Alternating>::remap_xy(0, 48),
1762            (64, 16)
1763        );
1764    }
1765
1766    #[test]
1767    fn test_quarter_scan_remap_matches_inner_bitplane_geometry() {
1768        // The bitplane DmaFrameBuffer<NROWS, COLS, PLANES> exposes NROWS * 2
1769        // rows (one per channel), so a 64x64 1/16-scan panel needs NROWS = 16
1770        // (16 addresses) and COLS = 128 (two 64-column sections per channel).
1771        use crate::bitplane::plain::DmaFrameBuffer;
1772        type QS = QuarterScan<64, 64>;
1773        type InnerFB = DmaFrameBuffer<16, 128, 4>;
1774        type Display = RemappedFrameBuffer<InnerFB, QS>;
1775
1776        let mut fb = Display::new();
1777        assert_eq!(fb.size(), Size::new(64, 64));
1778        // one pixel in each group, verified through the remapped wrapper
1779        fb.set_pixel(Point::new(1, 2), Color::RED);
1780        fb.set_pixel(Point::new(1, 20), Color::GREEN);
1781        fb.set_pixel(Point::new(1, 40), Color::BLUE);
1782        fb.set_pixel(Point::new(1, 60), Color::WHITE);
1783    }
1784}