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