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