Skip to main content

rlvgl_platform/
blit.rs

1//! Basic graphics types and blitter traits for platform backends.
2//!
3//! These types describe pixel surfaces and operations that can be
4//! accelerated by different platform implementations.
5
6#[cfg(any(
7    feature = "canvas",
8    feature = "gif",
9    feature = "apng",
10    feature = "nes",
11    feature = "png",
12    feature = "jpeg",
13    feature = "qrcode",
14    feature = "lottie",
15    feature = "fontdue",
16    test,
17))]
18use alloc::vec::Vec;
19#[cfg(feature = "fontdue")]
20use alloc::{collections::BTreeMap, vec};
21use bitflags::bitflags;
22use heapless::Vec as HVec;
23use rlvgl_core::cmd::{BlendMode, Cmd, CommandList};
24use rlvgl_core::font::{FontMetrics, ShapedText};
25#[cfg(feature = "fontdue")]
26use rlvgl_core::fontdue::{Metrics, line_metrics, rasterize_glyph};
27use rlvgl_core::renderer::Renderer;
28use rlvgl_core::widget::{Color, Rect as WidgetRect};
29
30#[cfg(feature = "fontdue")]
31const FONT_DATA: &[u8] = include_bytes!("../assets/fonts/DejaVuSans.ttf");
32
33#[cfg(feature = "fontdue")]
34fn round_to_i32(value: f32) -> i32 {
35    if value.is_nan() {
36        0
37    } else if value >= 0.0 {
38        (value + 0.5) as i32
39    } else {
40        (value - 0.5) as i32
41    }
42}
43
44#[cfg(feature = "fontdue")]
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
46/// Key identifying a cached glyph by font, size, and character.
47struct GlyphKey {
48    /// Pointer to the font data used to rasterize the glyph.
49    font: *const u8,
50    /// Font size in pixels, stored as raw bits for ordering.
51    size: u32,
52    /// Unicode codepoint of the glyph.
53    ch: char,
54}
55
56/// Supported pixel formats.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum PixelFmt {
59    /// 32-bit ARGB8888 format.
60    Argb8888,
61    /// 16-bit RGB565 format.
62    Rgb565,
63    /// 8-bit grayscale format.
64    L8,
65    /// 8-bit alpha-only format.
66    A8,
67    /// 4-bit alpha-only format.
68    A4,
69}
70
71/// Rectangular region within a surface.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct Rect {
74    /// Left coordinate of the rectangle.
75    pub x: i32,
76    /// Top coordinate of the rectangle.
77    pub y: i32,
78    /// Width of the rectangle in pixels.
79    pub w: u32,
80    /// Height of the rectangle in pixels.
81    pub h: u32,
82}
83
84/// A pixel buffer with dimension and format metadata.
85pub struct Surface<'a> {
86    /// Underlying pixel storage.
87    pub buf: &'a mut [u8],
88    /// Number of bytes between consecutive lines.
89    pub stride: usize,
90    /// Pixel format used by the buffer.
91    pub format: PixelFmt,
92    /// Width of the surface in pixels.
93    pub width: u32,
94    /// Height of the surface in pixels.
95    pub height: u32,
96}
97
98impl<'a> Surface<'a> {
99    /// Create a new surface from raw parts.
100    pub fn new(
101        buf: &'a mut [u8],
102        stride: usize,
103        format: PixelFmt,
104        width: u32,
105        height: u32,
106    ) -> Self {
107        Self {
108            buf,
109            stride,
110            format,
111            width,
112            height,
113        }
114    }
115}
116
117bitflags! {
118    /// Capabilities supported by a blitter implementation.
119    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
120    pub struct BlitCaps: u32 {
121        /// Ability to fill regions with a solid color.
122        const FILL = 0b0001;
123        /// Ability to copy pixels between surfaces.
124        const BLIT = 0b0010;
125        /// Ability to blend a source over a destination.
126        const BLEND = 0b0100;
127        /// Ability to convert between pixel formats.
128        const PFC = 0b1000;
129    }
130}
131
132/// Trait implemented by types capable of transferring pixel data.
133pub trait Blitter {
134    /// Return the capabilities supported by this blitter.
135    fn caps(&self) -> BlitCaps;
136
137    /// Fill `area` within `dst` with a solid `color`.
138    fn fill(&mut self, dst: &mut Surface, area: Rect, color: u32);
139
140    /// Copy pixels from `src` within `src_area` to `dst` at `dst_pos`.
141    fn blit(&mut self, src: &Surface, src_area: Rect, dst: &mut Surface, dst_pos: (i32, i32));
142
143    /// Blend pixels from `src` over `dst`.
144    fn blend(&mut self, src: &Surface, src_area: Rect, dst: &mut Surface, dst_pos: (i32, i32));
145}
146
147/// Collects dirty rectangles for a frame and optionally coalesces them.
148///
149/// The planner stores up to `N` rectangles in a stack-allocated buffer. Call
150/// [`Self::add`] to register a region that changed during rendering and
151/// [`Self::rects`] to obtain the batched list for flushing. After presenting
152/// the frame, call [`Self::clear`] to reuse the planner for the next frame.
153///
154/// Adds beyond `N` are silently dropped, but [`Self::overflowed`] flips to
155/// `true` so callers driving a dirty-rect present path know the rect set is
156/// incomplete and can fall back to a full-frame repaint for that frame.
157pub struct BlitPlanner<const N: usize> {
158    rects: HVec<Rect, N>,
159    overflowed: bool,
160}
161
162impl<const N: usize> BlitPlanner<N> {
163    /// Create an empty planner.
164    pub fn new() -> Self {
165        Self {
166            rects: HVec::new(),
167            overflowed: false,
168        }
169    }
170
171    /// Record a dirty rectangle.
172    pub fn add(&mut self, rect: Rect) {
173        if self.rects.push(rect).is_err() {
174            self.overflowed = true;
175        }
176    }
177
178    /// Return all accumulated rectangles.
179    pub fn rects(&self) -> &[Rect] {
180        &self.rects
181    }
182
183    /// Whether [`Self::add`] dropped a rect because the planner was full.
184    pub fn overflowed(&self) -> bool {
185        self.overflowed
186    }
187
188    /// Remove all stored rectangles.
189    pub fn clear(&mut self) {
190        self.rects.clear();
191        self.overflowed = false;
192    }
193}
194
195impl<const N: usize> Default for BlitPlanner<N> {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201/// Renderer implementation backed by a [`Blitter`].
202///
203/// A `BlitterRenderer` owns a target [`Surface`] and batches dirty regions
204/// using a [`BlitPlanner`]. Widgets interact with the generic [`Renderer`] trait
205/// without being aware of the underlying blitter.
206pub struct BlitterRenderer<'a, B: Blitter, const N: usize> {
207    blitter: &'a mut B,
208    surface: Surface<'a>,
209    planner: BlitPlanner<N>,
210    #[cfg(any(
211        feature = "canvas",
212        feature = "gif",
213        feature = "apng",
214        feature = "nes",
215        all(feature = "png", not(target_os = "none")),
216        all(feature = "jpeg", not(target_os = "none")),
217        all(feature = "qrcode", not(target_os = "none")),
218        feature = "lottie",
219        test,
220    ))]
221    scratch: Option<Vec<u8>>,
222    #[cfg(feature = "fontdue")]
223    glyph_cache: BTreeMap<GlyphKey, (Metrics, Vec<u8>)>,
224}
225
226impl<'a, B: Blitter, const N: usize> BlitterRenderer<'a, B, N> {
227    /// Create a new renderer targeting `surface` using `blitter`.
228    pub fn new(blitter: &'a mut B, surface: Surface<'a>) -> Self {
229        Self {
230            blitter,
231            surface,
232            planner: BlitPlanner::new(),
233            #[cfg(any(
234                feature = "canvas",
235                feature = "gif",
236                feature = "apng",
237                feature = "nes",
238                all(feature = "png", not(target_os = "none")),
239                all(feature = "jpeg", not(target_os = "none")),
240                all(feature = "qrcode", not(target_os = "none")),
241                feature = "lottie",
242                test,
243            ))]
244            scratch: None,
245            #[cfg(feature = "fontdue")]
246            glyph_cache: BTreeMap::new(),
247        }
248    }
249
250    /// Access the internal dirty-rectangle planner.
251    pub fn planner(&mut self) -> &mut BlitPlanner<N> {
252        &mut self.planner
253    }
254
255    #[cfg(any(
256        feature = "canvas",
257        feature = "gif",
258        feature = "apng",
259        feature = "nes",
260        all(feature = "png", not(target_os = "none")),
261        all(feature = "jpeg", not(target_os = "none")),
262        all(feature = "qrcode", not(target_os = "none")),
263        feature = "lottie",
264        test,
265    ))]
266    fn blit_colors(&mut self, position: (i32, i32), pixels: &[Color], w: u32, h: u32) {
267        let required = (w * h * 4) as usize;
268        let buf = self.scratch.get_or_insert_with(Vec::new);
269        if buf.len() < required {
270            buf.resize(required, 0);
271        }
272        for (i, c) in pixels.iter().enumerate() {
273            buf[i * 4..i * 4 + 4].copy_from_slice(&c.to_argb8888().to_le_bytes());
274        }
275        let src = Surface::new(
276            &mut buf[..required],
277            (w * 4) as usize,
278            PixelFmt::Argb8888,
279            w,
280            h,
281        );
282        self.blitter
283            .blit(&src, Rect { x: 0, y: 0, w, h }, &mut self.surface, position);
284        self.planner.add(Rect {
285            x: position.0,
286            y: position.1,
287            w,
288            h,
289        });
290    }
291
292    #[cfg(all(feature = "png", not(target_os = "none")))]
293    /// Decode a PNG image and blit it onto the target surface.
294    pub fn draw_png(
295        &mut self,
296        position: (i32, i32),
297        data: &[u8],
298    ) -> Result<(), rlvgl_core::png::DecodingError> {
299        let (pixels, w, h) = rlvgl_core::png::decode(data)?;
300        self.blit_colors(position, &pixels, w, h);
301        Ok(())
302    }
303
304    #[cfg(all(feature = "jpeg", not(target_os = "none")))]
305    /// Decode a JPEG image and blit it onto the target surface.
306    pub fn draw_jpeg(
307        &mut self,
308        position: (i32, i32),
309        data: &[u8],
310    ) -> Result<(), rlvgl_core::jpeg::Error> {
311        let (pixels, w, h) = rlvgl_core::jpeg::decode(data)?;
312        self.blit_colors(position, &pixels, w as u32, h as u32);
313        Ok(())
314    }
315
316    #[cfg(all(feature = "qrcode", not(target_os = "none")))]
317    /// Generate a QR code from `data` and blit it onto the target surface.
318    pub fn draw_qr(
319        &mut self,
320        position: (i32, i32),
321        data: &[u8],
322    ) -> Result<(), rlvgl_core::qrcode::QrError> {
323        let (pixels, w, h) = rlvgl_core::qrcode::generate(data)?;
324        self.blit_colors(position, &pixels, w, h);
325        Ok(())
326    }
327
328    #[cfg(feature = "lottie")]
329    /// Render a Lottie JSON animation frame and blit it onto the target surface.
330    ///
331    /// Returns an error if the JSON data is invalid.
332    pub fn draw_lottie_frame(
333        &mut self,
334        position: (i32, i32),
335        json: &str,
336        frame: usize,
337        width: u32,
338        height: u32,
339    ) -> Result<(), rlvgl_core::lottie::Error> {
340        let pixels =
341            rlvgl_core::lottie::render_lottie_frame(json, frame, width as usize, height as usize)?;
342        self.blit_colors(position, &pixels, width, height);
343        Ok(())
344    }
345
346    #[cfg(feature = "canvas")]
347    /// Blit an [`rlvgl_core::canvas::Canvas`] onto the target surface.
348    pub fn draw_canvas(&mut self, position: (i32, i32), canvas: &rlvgl_core::canvas::Canvas) {
349        let (w, h) = canvas.size();
350        let pixels = canvas.pixels();
351        self.blit_colors(position, &pixels, w, h);
352    }
353
354    #[cfg(feature = "gif")]
355    /// Decode a GIF and blit the selected frame onto the target surface.
356    pub fn draw_gif_frame(
357        &mut self,
358        position: (i32, i32),
359        data: &[u8],
360        frame: usize,
361    ) -> Result<(), rlvgl_core::gif::DecodingError> {
362        let (frames, w, h) = rlvgl_core::gif::decode(data)?;
363        if let Some(f) = frames.get(frame) {
364            self.blit_colors(position, &f.pixels, w as u32, h as u32);
365        }
366        Ok(())
367    }
368
369    #[cfg(feature = "apng")]
370    /// Decode an APNG and blit the selected frame onto the target surface.
371    pub fn draw_apng_frame(
372        &mut self,
373        position: (i32, i32),
374        data: &[u8],
375        frame: usize,
376    ) -> Result<(), image::ImageError> {
377        let (frames, w, h) = rlvgl_core::apng::decode(data)?;
378        if let Some(f) = frames.get(frame) {
379            self.blit_colors(position, &f.pixels, w, h);
380        }
381        Ok(())
382    }
383
384    #[cfg(all(feature = "pinyin", feature = "fontdue"))]
385    /// Render Pinyin IME candidate characters via the blitter.
386    ///
387    /// Returns `true` if any candidates were rendered for `input`.
388    pub fn draw_pinyin_candidates(
389        &mut self,
390        position: (i32, i32),
391        ime: &rlvgl_core::pinyin::PinyinInputMethod,
392        input: &str,
393        color: Color,
394    ) -> bool {
395        if let Some(chars) = ime.candidates(input) {
396            // Determine remaining space on the surface.
397            let max_w = self.surface.width as i32 - position.0;
398            let max_h = self.surface.height as i32 - position.1;
399            if max_w <= 0 || max_h < 16 {
400                return false;
401            }
402
403            // Truncate the candidate string to fit within the surface width.
404            let text: alloc::string::String = chars.into_iter().collect();
405            let max_chars = (max_w / 16) as usize;
406            let clipped: alloc::string::String = text.chars().take(max_chars).collect();
407            if clipped.is_empty() {
408                return false;
409            }
410            Renderer::draw_text(self, position, &clipped, color);
411            true
412        } else {
413            false
414        }
415    }
416
417    #[cfg(all(feature = "fatfs", feature = "fontdue"))]
418    /// List a FAT directory and render the entries line by line.
419    pub fn draw_fatfs_dir<T>(
420        &mut self,
421        position: (i32, i32),
422        image: &mut T,
423        dir: &str,
424        color: Color,
425    ) -> Result<(), std::io::Error>
426    where
427        T: std::io::Read + std::io::Write + std::io::Seek,
428    {
429        let max_w = self.surface.width as i32 - position.0;
430        let max_h = self.surface.height as i32 - position.1;
431        if max_w <= 0 || max_h <= 0 {
432            return Ok(());
433        }
434        let line_h = 16;
435        let max_lines = (max_h / line_h) as usize;
436        let max_chars = (max_w / 16) as usize;
437        let names = rlvgl_core::fatfs::list_dir(image, dir)?;
438        for (i, name) in names.iter().take(max_lines).enumerate() {
439            let y = position.1 + (i as i32) * line_h;
440            let clipped: alloc::string::String = name.chars().take(max_chars).collect();
441            if clipped.is_empty() {
442                break;
443            }
444            Renderer::draw_text(self, (position.0, y), &clipped, color);
445        }
446        Ok(())
447    }
448
449    #[cfg(feature = "nes")]
450    /// Blit an NES frame represented as ARGB8888 [`Color`] pixels.
451    pub fn draw_nes_frame(
452        &mut self,
453        position: (i32, i32),
454        pixels: &[Color],
455        width: u32,
456        height: u32,
457    ) {
458        self.blit_colors(position, pixels, width, height);
459    }
460
461    #[cfg(feature = "fontdue")]
462    /// Draw UTF-8 text using the supplied font and size.
463    pub fn draw_text(
464        &mut self,
465        position: (i32, i32),
466        text: &str,
467        color: Color,
468        font_data: &[u8],
469        px: f32,
470    ) {
471        let vm = line_metrics(font_data, px).unwrap();
472        let ascent = round_to_i32(vm.ascent);
473        let baseline = position.1 + ascent;
474        let mut x_cursor = position.0;
475        for ch in text.chars() {
476            let key = GlyphKey {
477                font: font_data.as_ptr(),
478                size: px.to_bits(),
479                ch,
480            };
481            let (metrics, bitmap) = {
482                let entry = self
483                    .glyph_cache
484                    .entry(key)
485                    .or_insert_with(|| rasterize_glyph(font_data, ch, px).unwrap());
486                (entry.0, entry.1.clone())
487            };
488            let w = metrics.width as i32;
489            let h = metrics.height as i32;
490            if w == 0 || h == 0 {
491                x_cursor += round_to_i32(metrics.advance_width);
492                continue;
493            }
494            let mut argb = vec![0u8; (w * h * 4) as usize];
495            for y in 0..h {
496                for x in 0..w {
497                    let alpha = bitmap[(y) as usize * metrics.width + x as usize];
498                    let idx = ((y * w + x) * 4) as usize;
499                    argb[idx] = (color.0 as u16 * alpha as u16 / 255) as u8;
500                    argb[idx + 1] = (color.1 as u16 * alpha as u16 / 255) as u8;
501                    argb[idx + 2] = (color.2 as u16 * alpha as u16 / 255) as u8;
502                    argb[idx + 3] = alpha;
503                }
504            }
505            let src = Surface::new(
506                argb.as_mut_slice(),
507                (w * 4) as usize,
508                PixelFmt::Argb8888,
509                w as u32,
510                h as u32,
511            );
512            let dst_pos = (
513                x_cursor + metrics.xmin,
514                baseline - ascent - metrics.ymin - (h - 1),
515            );
516            self.blitter.blend(
517                &src,
518                Rect {
519                    x: 0,
520                    y: 0,
521                    w: w as u32,
522                    h: h as u32,
523                },
524                &mut self.surface,
525                dst_pos,
526            );
527            self.planner.add(Rect {
528                x: dst_pos.0,
529                y: dst_pos.1,
530                w: w as u32,
531                h: h as u32,
532            });
533            x_cursor += round_to_i32(metrics.advance_width);
534        }
535    }
536
537    #[cfg(not(feature = "fontdue"))]
538    /// Stub text renderer when fontdue is disabled.
539    pub fn draw_text(
540        &mut self,
541        position: (i32, i32),
542        text: &str,
543        color: Color,
544        _font_data: &[u8],
545        _px: f32,
546    ) {
547        let _ = (position, text, color);
548    }
549}
550
551impl<B: Blitter, const N: usize> Renderer for BlitterRenderer<'_, B, N> {
552    fn blend_rect(&mut self, rect: WidgetRect, color: Color) {
553        let alpha = color.3 as u16;
554        if alpha == 0 {
555            return;
556        }
557        if alpha == 255 {
558            self.fill_rect(rect, color);
559            return;
560        }
561        // Inline source-over blending for ARGB8888 surfaces.
562        if self.surface.format == PixelFmt::Argb8888 {
563            let sw = self.surface.width as i32;
564            let sh = self.surface.height as i32;
565            let stride = self.surface.stride;
566            let inv = 255 - alpha;
567            let x0 = rect.x.max(0);
568            let y0 = rect.y.max(0);
569            let x1 = (rect.x + rect.width).min(sw);
570            let y1 = (rect.y + rect.height).min(sh);
571            for y in y0..y1 {
572                for x in x0..x1 {
573                    let off = y as usize * stride + x as usize * 4;
574                    let bg_b = self.surface.buf[off] as u16;
575                    let bg_g = self.surface.buf[off + 1] as u16;
576                    let bg_r = self.surface.buf[off + 2] as u16;
577                    self.surface.buf[off] = ((color.2 as u16 * alpha + bg_b * inv) / 255) as u8;
578                    self.surface.buf[off + 1] = ((color.1 as u16 * alpha + bg_g * inv) / 255) as u8;
579                    self.surface.buf[off + 2] = ((color.0 as u16 * alpha + bg_r * inv) / 255) as u8;
580                    self.surface.buf[off + 3] = 0xff;
581                }
582            }
583            self.planner.add(Rect {
584                x: rect.x,
585                y: rect.y,
586                w: rect.width as u32,
587                h: rect.height as u32,
588            });
589        } else {
590            // Fallback for non-ARGB8888 surfaces: just overwrite.
591            self.fill_rect(rect, color);
592        }
593    }
594
595    fn fill_rect(&mut self, rect: WidgetRect, color: Color) {
596        let r = Rect {
597            x: rect.x,
598            y: rect.y,
599            w: rect.width as u32,
600            h: rect.height as u32,
601        };
602        self.planner.add(r);
603        self.blitter.fill(&mut self.surface, r, color.to_argb8888());
604    }
605
606    fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
607        #[cfg(feature = "fontdue")]
608        {
609            const PX: f32 = 16.0;
610            BlitterRenderer::draw_text(self, position, text, color, FONT_DATA, PX);
611        }
612        #[cfg(not(feature = "fontdue"))]
613        {
614            let _ = (position, text, color);
615        }
616    }
617
618    fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
619        // Fast path: write ARGB8888 pixels directly into the surface buffer,
620        // then record the dirty rectangle. Avoids per-pixel fill_rect overhead.
621        if self.surface.format == PixelFmt::Argb8888 {
622            let sw = self.surface.width as i32;
623            let sh = self.surface.height as i32;
624            let stride = self.surface.stride;
625            for y in 0..height as i32 {
626                let dy = position.1 + y;
627                if dy < 0 || dy >= sh {
628                    continue;
629                }
630                for x in 0..width as i32 {
631                    let dx = position.0 + x;
632                    if dx < 0 || dx >= sw {
633                        continue;
634                    }
635                    let src_idx = (y as u32 * width + x as u32) as usize;
636                    if let Some(&c) = pixels.get(src_idx) {
637                        let off = dy as usize * stride + dx as usize * 4;
638                        self.surface.buf[off..off + 4]
639                            .copy_from_slice(&c.to_argb8888().to_le_bytes());
640                    }
641                }
642            }
643            self.planner.add(Rect {
644                x: position.0,
645                y: position.1,
646                w: width,
647                h: height,
648            });
649        } else {
650            // Fallback: per-pixel fill_rect for non-ARGB8888 surfaces
651            for y in 0..height as i32 {
652                for x in 0..width as i32 {
653                    let idx = (y as u32 * width + x as u32) as usize;
654                    if let Some(&c) = pixels.get(idx) {
655                        self.fill_rect(
656                            WidgetRect {
657                                x: position.0 + x,
658                                y: position.1 + y,
659                                width: 1,
660                                height: 1,
661                            },
662                            c,
663                        );
664                    }
665                }
666            }
667        }
668    }
669
670    fn blend_row(&mut self, x: i32, y: i32, color: Color, coverage: &[u8]) {
671        // AA inner-loop primitive. Direct ARGB8888 source-over with per-pixel
672        // alpha = (color.alpha * coverage[i]) / 255. One planner.add per row
673        // keeps DMA2D refresh granularity efficient versus per-pixel adds.
674        if color.3 == 0 || coverage.is_empty() {
675            return;
676        }
677        if self.surface.format != PixelFmt::Argb8888 {
678            // Non-ARGB8888 surfaces: fall back through the trait default
679            // (per-pixel blend_rect). This branch is rare on disco.
680            for (i, &cov) in coverage.iter().enumerate() {
681                if cov == 0 {
682                    continue;
683                }
684                let alpha = ((color.3 as u16 * cov as u16) / 255) as u8;
685                if alpha == 0 {
686                    continue;
687                }
688                self.blend_rect(
689                    WidgetRect {
690                        x: x + i as i32,
691                        y,
692                        width: 1,
693                        height: 1,
694                    },
695                    Color(color.0, color.1, color.2, alpha),
696                );
697            }
698            return;
699        }
700        let sw = self.surface.width as i32;
701        let sh = self.surface.height as i32;
702        if y < 0 || y >= sh {
703            return;
704        }
705        let stride = self.surface.stride;
706        let src_a = color.3 as u16;
707        let row_off = y as usize * stride;
708        let mut written_x0 = i32::MAX;
709        let mut written_x1 = i32::MIN;
710        for (i, &cov) in coverage.iter().enumerate() {
711            if cov == 0 {
712                continue;
713            }
714            let px = x + i as i32;
715            if px < 0 || px >= sw {
716                continue;
717            }
718            let alpha = (src_a * cov as u16) / 255;
719            if alpha == 0 {
720                continue;
721            }
722            let inv = 255 - alpha;
723            let off = row_off + (px as usize) * 4;
724            let bg_b = self.surface.buf[off] as u16;
725            let bg_g = self.surface.buf[off + 1] as u16;
726            let bg_r = self.surface.buf[off + 2] as u16;
727            self.surface.buf[off] = ((color.2 as u16 * alpha + bg_b * inv) / 255) as u8;
728            self.surface.buf[off + 1] = ((color.1 as u16 * alpha + bg_g * inv) / 255) as u8;
729            self.surface.buf[off + 2] = ((color.0 as u16 * alpha + bg_r * inv) / 255) as u8;
730            self.surface.buf[off + 3] = 0xff;
731            if px < written_x0 {
732                written_x0 = px;
733            }
734            if px > written_x1 {
735                written_x1 = px;
736            }
737        }
738        if written_x0 <= written_x1 {
739            self.planner.add(Rect {
740                x: written_x0,
741                y,
742                w: (written_x1 - written_x0 + 1) as u32,
743                h: 1,
744            });
745        }
746    }
747
748    fn submit(&mut self, list: &CommandList) {
749        submit_with_occlusion(self, list);
750    }
751}
752
753/// `CommandSink` impl with occlusion pre-pass.
754///
755/// Backend-specialized submit path: walks the [`CommandList`] and, for
756/// each cmd, checks whether any *later* opaque [`Cmd::FillRect`] fully
757/// covers its AABB. If so, the cmd is skipped entirely — it would be
758/// overwritten anyway. Survivors are dispatched via the normal
759/// [`Renderer`] trait methods on `self` (existing
760/// [`BlitterRenderer::blend_row`] override applies, so AA cmds get the
761/// hardware-blend fast path).
762///
763/// The check is O(n²) but cmd counts are small (a typical clock frame
764/// is 50–100 cmds). Only `FillRect` with `alpha == 255` qualifies as an
765/// occluder — geometric primitives (OBB, disc, arc, line) don't fully
766/// cover their AABBs and are never occluders even at full alpha.
767///
768/// Two-pass dispatch:
769///
770/// 1. **Occlusion pre-pass.** For each cmd, scan forward looking for a
771///    later opaque [`Cmd::FillRect`] whose AABB fully covers it; if
772///    found, drop the cmd. Markers (`Barrier`, `SetClip`) terminate
773///    the search range — a `Barrier` declares ordering that must be
774///    preserved (compositor sync, swap hold), and a `SetClip` change
775///    means later cmds render under a different clip and can't be
776///    trusted as occluders of earlier pixels.
777///
778/// 2. **Adjacent-FillRect coalescing.** Survivor cmds are dispatched
779///    one at a time, but consecutive [`Cmd::FillRect`]s with identical
780///    `color` + `blend` and edge-adjacent rectangles are merged into a
781///    single wider FillRect. Cuts down on the number of DMA2D fill
782///    ops and reduces dirty-rect tracking pressure when widgets
783///    happen to emit sequential strips of the same fill (toolbars,
784///    background bands, table cells).
785///
786/// The coalescing is conservative — only horizontal or vertical edge
787/// adjacency is detected (no L-shapes, no overlap-into-union). For
788/// the common case of a row of cells fixed-width and same-color, this
789/// reduces N rects to 1.
790fn submit_with_occlusion<B: Blitter, const N: usize>(
791    r: &mut BlitterRenderer<'_, B, N>,
792    list: &CommandList,
793) {
794    let mut pending: Option<(WidgetRect, Color, BlendMode)> = None;
795
796    for (i, cmd) in list.iter().enumerate() {
797        // Occlusion pre-pass.
798        let occluded = match cmd.aabb() {
799            None => false,
800            Some(bb_i) => {
801                let mut hit = false;
802                for later in list.iter().skip(i + 1) {
803                    if matches!(later, Cmd::Barrier | Cmd::SetClip { .. }) {
804                        break;
805                    }
806                    if !is_opaque_filler(later) {
807                        continue;
808                    }
809                    let Some(bb_j) = later.aabb() else { continue };
810                    if rect_contains(bb_j, bb_i) {
811                        hit = true;
812                        break;
813                    }
814                }
815                hit
816            }
817        };
818        if occluded {
819            // Skip the cmd entirely. Don't flush pending — the next cmd
820            // may still coalesce with what's pending across this gap.
821            continue;
822        }
823
824        // Coalescing dispatch.
825        if let Cmd::FillRect { rect, color, blend } = cmd {
826            if let Some((prect, pcolor, pblend)) = pending.as_ref() {
827                if pcolor == color
828                    && pblend == blend
829                    && let Some(merged) = merge_rects_axis_aligned(*prect, *rect)
830                {
831                    pending = Some((merged, *color, *blend));
832                    continue;
833                }
834                // Can't merge — flush pending and start a new pending.
835                flush_pending(r, pending.take());
836            }
837            pending = Some((*rect, *color, *blend));
838        } else {
839            // Markers and non-FillRect cmds break the coalescing run.
840            flush_pending(r, pending.take());
841            cmd.dispatch_to(r);
842        }
843    }
844    flush_pending(r, pending.take());
845}
846
847#[inline]
848fn flush_pending<B: Blitter, const N: usize>(
849    r: &mut BlitterRenderer<'_, B, N>,
850    pending: Option<(WidgetRect, Color, BlendMode)>,
851) {
852    if let Some((rect, color, blend)) = pending {
853        Cmd::FillRect { rect, color, blend }.dispatch_to(r);
854    }
855}
856
857/// Try to merge two rectangles into a single rectangle representing
858/// their union. Returns `Some(union)` only when one of the four
859/// edge-adjacent cases applies; returns `None` for overlap, gap, or
860/// L-shapes (which would require a multi-rect representation).
861#[inline]
862fn merge_rects_axis_aligned(a: WidgetRect, b: WidgetRect) -> Option<WidgetRect> {
863    if a.y == b.y && a.height == b.height {
864        if a.x + a.width == b.x {
865            return Some(WidgetRect {
866                x: a.x,
867                y: a.y,
868                width: a.width + b.width,
869                height: a.height,
870            });
871        }
872        if b.x + b.width == a.x {
873            return Some(WidgetRect {
874                x: b.x,
875                y: b.y,
876                width: a.width + b.width,
877                height: a.height,
878            });
879        }
880    }
881    if a.x == b.x && a.width == b.width {
882        if a.y + a.height == b.y {
883            return Some(WidgetRect {
884                x: a.x,
885                y: a.y,
886                width: a.width,
887                height: a.height + b.height,
888            });
889        }
890        if b.y + b.height == a.y {
891            return Some(WidgetRect {
892                x: b.x,
893                y: b.y,
894                width: a.width,
895                height: a.height + b.height,
896            });
897        }
898    }
899    None
900}
901
902#[inline]
903fn is_opaque_filler(cmd: &Cmd) -> bool {
904    if let Cmd::FillRect { color, .. } = cmd {
905        color.3 == 255
906    } else {
907        false
908    }
909}
910
911#[inline]
912fn rect_contains(outer: WidgetRect, inner: WidgetRect) -> bool {
913    outer.x <= inner.x
914        && outer.y <= inner.y
915        && outer.x + outer.width >= inner.x + inner.width
916        && outer.y + outer.height >= inner.y + inner.height
917}
918
919/// Renderer wrapper that applies 90° CCW rotation for platforms where the
920/// physical display is landscape but the framebuffer is portrait.
921///
922/// Maps logical coordinates (800×480 landscape) to framebuffer coordinates
923/// (480×800 portrait):
924///   fb_x = fb_width - logical_y - logical_height
925///   fb_y = logical_x
926///   fb_w = logical_height
927///   fb_h = logical_width
928pub struct RotatedRenderer<'a> {
929    inner: &'a mut dyn Renderer,
930    /// Portrait framebuffer width (the short dimension, e.g. 480).
931    fb_width: i32,
932}
933
934impl<'a> RotatedRenderer<'a> {
935    /// Create a rotated renderer wrapping `inner`.
936    ///
937    /// `fb_width` is the portrait framebuffer width (480 on STM32H747I-DISCO).
938    pub fn new(inner: &'a mut dyn Renderer, fb_width: u32) -> Self {
939        Self {
940            inner,
941            fb_width: fb_width as i32,
942        }
943    }
944
945    /// FONT-04 §8.B: rotate a glyph's A8 coverage once into a bounded scratch
946    /// buffer and emit it as physical (portrait) rows through
947    /// `inner.blend_row`, instead of dispatching every coverage pixel through
948    /// the rotation as a 1×1 `blend_rect`.
949    ///
950    /// `extent` is the glyph's tight bitmap rect in *landscape* coordinates.
951    /// The rotation mirrors [`Self::draw_pixels`] / the DMA2D
952    /// `draw_glyph_rotated` reference: landscape `(col,row)` →
953    /// `scratch[col*h + (h-1-row)]`, physical size `h` wide × `w` tall, placed
954    /// at `(fb_width - extent.y - h, extent.x)`. Because `inner.blend_row`
955    /// performs the same per-pixel source-over (`alpha = color.a·cov/255`) the
956    /// per-pixel path would, the rendered pixels are identical — this is a
957    /// throughput change only (§8.A).
958    ///
959    /// Returns `false` when `font` has no coverage for `ch` (the caller falls
960    /// back to an extent rect), matching the core `draw_glyph` contract.
961    fn blit_glyph_coverage_rotated(
962        &mut self,
963        font: &dyn FontMetrics,
964        ch: char,
965        extent: WidgetRect,
966        color: Color,
967    ) -> bool {
968        let w = extent.width;
969        let h = extent.height;
970        if w <= 0 || h <= 0 {
971            return true; // zero-area glyph (e.g. space) — nothing to draw
972        }
973        let (wu, hu) = (w as usize, h as usize);
974
975        // Bounded scratch (§8.C): no per-glyph heap in the render loop.
976        const BOUND: usize = 64 * 64;
977        if wu <= 64 && hu <= 64 && wu * hu <= BOUND {
978            // 2026-06-15 (FONT-04): A8 coverage scratch, sibling to the Color
979            // SCRATCH in `draw_pixels`. Same `.rlvgl_blit_scratch` placement so
980            // consumers' linker scripts keep it off the MSP stack-growth path
981            // (see the draw_pixels SCRATCH comment + disco ERRATA-005).
982            #[cfg_attr(target_os = "none", unsafe(link_section = ".rlvgl_blit_scratch"))]
983            static mut SCRATCH_COV: [u8; BOUND] = [0u8; BOUND]; // rlvgl-discipline: allow(static_mut)
984
985            // SAFETY: single-core, single-threaded — `RotatedRenderer` is used
986            // only from the main render loop, never an ISR. The scratch is
987            // unique to this call site, not borrowed across calls; we fully
988            // write the `[..wu*hu]` prefix before reading it back below.
989            let scratch = unsafe { &mut SCRATCH_COV[..wu * hu] };
990
991            // Gather each landscape coverage row and scatter it into the
992            // rotated (portrait) layout in one pass.
993            let mut row_buf = [0u8; 64];
994            for row in 0..hu {
995                let cov_row = &mut row_buf[..wu];
996                if !font.glyph_coverage_row(ch, row as u16, 0, cov_row) {
997                    return false;
998                }
999                for (col, &cov) in cov_row.iter().enumerate() {
1000                    scratch[col * hu + (hu - 1 - row)] = cov;
1001                }
1002            }
1003
1004            // Physical placement mirrors `fill_rect`/`draw_pixels`.
1005            let fb_x = self.fb_width - extent.y - h;
1006            let fb_y = extent.x;
1007            // Emit `w` physical rows of `h` coverage bytes each; inner clips x
1008            // and blends source-over (preserving cov-0 holes).
1009            for py in 0..wu {
1010                let run = &scratch[py * hu..py * hu + hu];
1011                self.inner.blend_row(fb_x, fb_y + py as i32, color, run);
1012            }
1013            true
1014        } else {
1015            // Oversize fallback: the correct per-pixel rotated path
1016            // (`self.blend_row` default → rotated 1×1 `blend_rect`). Rare — the
1017            // disco widget path draws no glyphs this large.
1018            let mut chunk = [0u8; 64];
1019            for row in 0..hu {
1020                let mut x_off = 0usize;
1021                while x_off < wu {
1022                    let n = (wu - x_off).min(chunk.len());
1023                    let part = &mut chunk[..n];
1024                    if !font.glyph_coverage_row(ch, row as u16, x_off as u16, part) {
1025                        return false;
1026                    }
1027                    self.blend_row(extent.x + x_off as i32, extent.y + row as i32, color, part);
1028                    x_off += n;
1029                }
1030            }
1031            true
1032        }
1033    }
1034}
1035
1036impl Renderer for RotatedRenderer<'_> {
1037    fn fill_rect(&mut self, rect: WidgetRect, color: Color) {
1038        let mut fb_x = self.fb_width - rect.y - rect.height;
1039        let fb_y = rect.x;
1040        let mut fb_w = rect.height;
1041        let fb_h = rect.width;
1042
1043        if fb_w <= 0 || fb_h <= 0 {
1044            return;
1045        }
1046
1047        // Clamp left edge: rect extends off-screen left
1048        if fb_x < 0 {
1049            fb_w += fb_x; // shrink width by the overshoot
1050            fb_x = 0;
1051        }
1052        // Clamp right edge
1053        if fb_x + fb_w > self.fb_width {
1054            fb_w = self.fb_width - fb_x;
1055        }
1056        if fb_w <= 0 {
1057            return;
1058        }
1059
1060        self.inner.fill_rect(
1061            WidgetRect {
1062                x: fb_x,
1063                y: fb_y,
1064                width: fb_w,
1065                height: fb_h,
1066            },
1067            color,
1068        );
1069    }
1070
1071    fn blend_rect(&mut self, rect: WidgetRect, color: Color) {
1072        let mut fb_x = self.fb_width - rect.y - rect.height;
1073        let fb_y = rect.x;
1074        let mut fb_w = rect.height;
1075        let fb_h = rect.width;
1076
1077        if fb_w <= 0 || fb_h <= 0 {
1078            return;
1079        }
1080
1081        if fb_x < 0 {
1082            fb_w += fb_x;
1083            fb_x = 0;
1084        }
1085        if fb_x + fb_w > self.fb_width {
1086            fb_w = self.fb_width - fb_x;
1087        }
1088        if fb_w <= 0 {
1089            return;
1090        }
1091
1092        self.inner.blend_rect(
1093            WidgetRect {
1094                x: fb_x,
1095                y: fb_y,
1096                width: fb_w,
1097                height: fb_h,
1098            },
1099            color,
1100        );
1101    }
1102
1103    fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
1104        let fx = self.fb_width - 1 - position.1;
1105        if fx >= 0 {
1106            self.inner.draw_text((fx, position.0), text, color);
1107        }
1108    }
1109
1110    fn draw_glyph(&mut self, font: &dyn FontMetrics, ch: char, origin: (i32, i32), color: Color) {
1111        // FONT-04 §8.B: rotate the glyph's coverage once and blit rows through
1112        // `inner.blend_row`, rather than the per-pixel `blend_rect` the
1113        // trait-default `draw_glyph` → `blend_row` would dispatch on this
1114        // rotated wrapper. `origin` is the baseline pen position (same anchor
1115        // as the core default); derive the landscape extent from the metrics.
1116        let Some(info) = font.glyph_metrics(ch) else {
1117            return;
1118        };
1119        let extent = WidgetRect {
1120            x: origin.0 + info.bearing_x as i32,
1121            y: origin.1 - info.bearing_y as i32,
1122            width: info.width as i32,
1123            height: info.height as i32,
1124        };
1125        if extent.width <= 0 || extent.height <= 0 {
1126            return;
1127        }
1128        if !self.blit_glyph_coverage_rotated(font, ch, extent, color) {
1129            // No coverage for this glyph: deterministic extent rect (rotated),
1130            // matching the core `draw_glyph` fallback.
1131            self.blend_rect(extent, color);
1132        }
1133    }
1134
1135    fn draw_text_shaped(&mut self, shaped: &ShapedText<'_>, origin: (i32, i32), color: Color) {
1136        // Mirror the core default's per-glyph structure, but route coverage
1137        // through the rotate-then-blit helper (FONT-04 §8.B). Reached by
1138        // callers that draw onto the rotated renderer without an intervening
1139        // clip wrapper; clip-wrapped widgets (e.g. `Label`) still funnel
1140        // through `blend_row` and are unaffected.
1141        let font = shaped.font;
1142        for glyph in &shaped.glyphs {
1143            let mut extent = glyph.extent();
1144            extent.x += origin.0;
1145            extent.y += origin.1;
1146            if extent.width <= 0 || extent.height <= 0 {
1147                continue;
1148            }
1149            let drawn = match font {
1150                Some(font) => self.blit_glyph_coverage_rotated(font, glyph.ch, extent, color),
1151                None => false,
1152            };
1153            if !drawn {
1154                self.blend_rect(extent, color);
1155            }
1156        }
1157    }
1158
1159    fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
1160        // 2026-05-17: replace the per-pixel `fill_rect` slow path with a
1161        // rotate-then-delegate pattern. The previous implementation made
1162        // W*H calls to self.fill_rect (each a 1×1 logical rect) through
1163        // the full rotation + clip + blitter pipeline. For a 60×60 icon
1164        // that's 3600 single-pixel fill_rects per draw, and IconStrip
1165        // draws 3 icons per frame — pinning CM7 inside the blitter for
1166        // seconds at a time and making poll_joystick effectively
1167        // unreachable (probe-rs halt evidence: 3 sequential halts ~1s
1168        // apart all landed at byte-identical PC/LR/SP inside
1169        // CpuBlitter::fill's row loop, called from this site).
1170        //
1171        // The inner `BlitterRenderer::draw_pixels` (line 617) already has
1172        // an Argb8888 fast path that writes pixels directly into the
1173        // surface buffer + adds one dirty rect — H+W work per call, no
1174        // per-pixel fill_rect dispatch. Rotating the source buffer once
1175        // (W*H copies) and calling inner.draw_pixels once collapses
1176        // 3 × 60 × 60 = 10,800 fill_rect dispatches down to 3 fast blits.
1177        if width == 0 || height == 0 {
1178            return;
1179        }
1180
1181        // 90° rotation mapping (mirrors `fill_rect` above):
1182        // physical (fb_x, fb_y) = (fb_width - rect.y - rect.height, rect.x)
1183        // physical (width, height) = (rect.height, rect.width)
1184        let fb_x = self.fb_width - position.1 - height as i32;
1185        let fb_y = position.0;
1186        let phys_w = height; // physical width = logical height
1187        let phys_h = width; // physical height = logical width
1188
1189        // Re-lay-out the pixel buffer in physical orientation.
1190        // For each logical (lx, ly), the rotated buffer index is
1191        // (lx * phys_w) + (height - 1 - ly).
1192        //
1193        // 2026-05-17 (second pass): use a static scratch buffer instead
1194        // of allocating a Vec per call. The bench-9-snapshot setup
1195        // draws ~10-14 icons per render through this path
1196        // (IconStrip + Wing + spectrum overlay); at 9 Hz that was
1197        // ~200 KiB/sec of alloc/free through the 64 KiB linked-list
1198        // heap. After a few seconds of operation the heap fragments
1199        // and a subsequent alloc panics — observed as a "lock up after
1200        // navigating into the wing menu, recover via reset button"
1201        // pattern.
1202        //
1203        // The scratch buffer is sized for the worst case we draw in
1204        // practice (icons up to 64×64). If a caller requests a larger
1205        // pixels rect, we fall back to allocating one Vec for that
1206        // specific call — large icons are rare and the alloc still
1207        // makes progress.
1208        const SCRATCH_PIXELS: usize = 64 * 64;
1209        // 2026-05-25 — Place SCRATCH in a named link section so consumers
1210        // can map it to RAM that is NOT adjacent to MSP stack growth.
1211        //
1212        // Why this matters: on Cortex-M with cortex-m-rt's default link.x,
1213        // MSP starts at `ORIGIN(RAM) + LENGTH(RAM)` and grows DOWN through
1214        // `.bss`. This 16 KiB array (= `[Color; 64*64]` = 16384 bytes) in
1215        // `.bss` ends up near MSP's growth path on boards with modest RAM.
1216        // On STM32H747I-DISCO (DTCM = 128 KiB), the first render frame's
1217        // call chain consumes ~14.5 KiB of MSP via the widget-tree walk,
1218        // putting SP INSIDE this array. Inner-loop writes to `scratch[N]`
1219        // for large enough N then overwrite the function's own stack
1220        // frame — including the slice's fat pointer — leading to seemingly
1221        // random hard faults at addresses that look unrelated to the bug.
1222        //
1223        // Consumers MUST add a SECTIONS block mapping `.rlvgl_blit_scratch`
1224        // to a non-stack-adjacent RAM region in their linker script.
1225        // Example for STM32H747 (place in D1 AXI SRAM):
1226        //
1227        //   SECTIONS {
1228        //     .rlvgl_blit_scratch (NOLOAD) : ALIGN(4) {
1229        //       *(.rlvgl_blit_scratch .rlvgl_blit_scratch.*)
1230        //     } > D1_CM7
1231        //   } INSERT AFTER .uninit;
1232        //
1233        // Diagnosed in disco-analyzer ERRATA-005 bench-45 (2026-05-25):
1234        // SCRATCH lived at DTCM 0x2001_A459..0x2001_E459; SP at fault was
1235        // 0x2001_C5F8 (= inside the array); the slice's fat pointer slot
1236        // at sp+176 = scratch[2196] was overwritten by an icon pixel and
1237        // the next iteration's load of scratch.data_ptr returned the
1238        // trampled bytes (0xFFFFFF59) — a precise data-bus error in the
1239        // PPB region followed. Per-iteration capture of scratch.as_ptr()
1240        // confirmed the corruption point matched the icon pixel pattern
1241        // byte-for-byte.
1242        // CRATES-CI-01: the ELF-style section name is only meaningful for
1243        // the firmware linker script above; on Mach-O hosts (macOS dev
1244        // boxes consuming rlvgl-platform from crates.io) it is rejected at
1245        // LLVM codegen ("mach-o section specifier requires a segment").
1246        // Gate the attribute to bare-metal targets so host builds place
1247        // SCRATCH in .bss as usual.
1248        #[cfg_attr(target_os = "none", unsafe(link_section = ".rlvgl_blit_scratch"))]
1249        static mut SCRATCH: [Color; SCRATCH_PIXELS] = [Color(0, 0, 0, 0); SCRATCH_PIXELS]; // rlvgl-discipline: allow(static_mut)
1250
1251        let len = (width * height) as usize;
1252        if len <= SCRATCH_PIXELS {
1253            // SAFETY: single-core, single-threaded — `draw_pixels` is
1254            // called only from the main render loop and not from any
1255            // ISR. The scratch buffer is unique to this call site and
1256            // not borrowed across calls.
1257            let scratch = unsafe { &mut SCRATCH[..len] };
1258            // Clear only the prefix we'll actually fill; pixels that
1259            // aren't written (the `pixels.get(src_idx)` None branch)
1260            // retain their prior value, but we touch every index in
1261            // the inner loop so that's safe.
1262            for ly in 0..height as i32 {
1263                for lx in 0..width as i32 {
1264                    let src_idx = (ly as u32 * width + lx as u32) as usize;
1265                    let dst_idx = (lx as u32 * phys_w + (height as i32 - 1 - ly) as u32) as usize;
1266                    scratch[dst_idx] = pixels.get(src_idx).copied().unwrap_or(Color(0, 0, 0, 0));
1267                }
1268            }
1269            self.inner
1270                .draw_pixels((fb_x, fb_y), scratch, phys_w, phys_h);
1271        } else {
1272            // Oversize fallback: allocate once for this call. Rare.
1273            let mut rotated: alloc::vec::Vec<Color> = alloc::vec::Vec::with_capacity(len);
1274            rotated.resize(len, Color(0, 0, 0, 0));
1275            for ly in 0..height as i32 {
1276                for lx in 0..width as i32 {
1277                    let src_idx = (ly as u32 * width + lx as u32) as usize;
1278                    let dst_idx = (lx as u32 * phys_w + (height as i32 - 1 - ly) as u32) as usize;
1279                    if let Some(&c) = pixels.get(src_idx) {
1280                        rotated[dst_idx] = c;
1281                    }
1282                }
1283            }
1284            self.inner
1285                .draw_pixels((fb_x, fb_y), &rotated, phys_w, phys_h);
1286        }
1287    }
1288
1289    fn submit(&mut self, list: &CommandList) {
1290        // Delegate to inner — preserves any backend-specific
1291        // optimizations (e.g. BlitterRenderer's occlusion pre-pass)
1292        // through the rotation wrapper.
1293        self.inner.submit(list);
1294    }
1295}
1296
1297#[cfg(test)]
1298mod scratch_tests {
1299    use super::*;
1300    use crate::cpu_blitter::CpuBlitter;
1301
1302    #[test]
1303    fn blit_colors_reuses_scratch_buffer() {
1304        let mut buf = [0u8; 4 * 4 * 4];
1305        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1306        let mut blit = CpuBlitter;
1307        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1308            BlitterRenderer::new(&mut blit, surface);
1309        let pixels = [Color(0, 0, 0, 0)];
1310        renderer.blit_colors((0, 0), &pixels, 1, 1);
1311        let first_ptr = renderer.scratch.as_ref().unwrap().as_ptr();
1312        renderer.blit_colors((1, 1), &pixels, 1, 1);
1313        let second_ptr = renderer.scratch.as_ref().unwrap().as_ptr();
1314        assert_eq!(first_ptr, second_ptr);
1315    }
1316}
1317
1318#[cfg(all(test, feature = "fontdue"))]
1319mod text_tests {
1320    use super::*;
1321    use crate::cpu_blitter::CpuBlitter;
1322
1323    #[test]
1324    fn blitter_draws_text() {
1325        let mut buf = [0u8; 64 * 64 * 4];
1326        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1327        let mut blit = CpuBlitter;
1328        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1329            BlitterRenderer::new(&mut blit, surface);
1330        Renderer::draw_text(&mut renderer, (0, 32), "A", Color(255, 255, 255, 255));
1331        assert!(buf.iter().any(|&p| p != 0));
1332    }
1333
1334    #[test]
1335    fn cache_accounts_for_size() {
1336        let mut buf = [0u8; 64 * 64 * 4];
1337        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1338        let mut blit = CpuBlitter;
1339        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1340            BlitterRenderer::new(&mut blit, surface);
1341        renderer.draw_text((0, 32), "Hi", Color(255, 255, 255, 255), FONT_DATA, 16.0);
1342        let len_after_small = renderer.glyph_cache.len();
1343        renderer.draw_text((0, 32), "Hi", Color(255, 255, 255, 255), FONT_DATA, 16.0);
1344        assert_eq!(len_after_small, renderer.glyph_cache.len());
1345        renderer.draw_text((0, 32), "Hi", Color(255, 255, 255, 255), FONT_DATA, 24.0);
1346        assert!(renderer.glyph_cache.len() > len_after_small);
1347    }
1348}
1349
1350#[cfg(all(test, feature = "png", not(target_os = "none")))]
1351mod png_tests {
1352    use super::*;
1353    use crate::cpu_blitter::CpuBlitter;
1354    use base64::Engine;
1355    use rlvgl_core::png::DecodingError;
1356
1357    const RED_DOT_PNG: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC";
1358
1359    #[test]
1360    fn blitter_draws_png() {
1361        let data = base64::engine::general_purpose::STANDARD
1362            .decode(RED_DOT_PNG)
1363            .unwrap();
1364        let mut buf = [0u8; 4 * 4 * 4];
1365        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1366        let mut blit = CpuBlitter;
1367        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1368            BlitterRenderer::new(&mut blit, surface);
1369        renderer.draw_png((0, 0), &data).unwrap();
1370        assert!(buf.iter().any(|&p| p != 0));
1371    }
1372
1373    #[test]
1374    fn blitter_rejects_invalid_png() {
1375        let mut buf = [0u8; 4 * 4 * 4];
1376        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1377        let mut blit = CpuBlitter;
1378        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1379            BlitterRenderer::new(&mut blit, surface);
1380        let err = renderer.draw_png((0, 0), b"not a png").unwrap_err();
1381        assert!(matches!(err, DecodingError::Format(_)));
1382    }
1383}
1384
1385#[cfg(all(test, feature = "jpeg", not(target_os = "none")))]
1386mod jpeg_tests {
1387    use super::*;
1388    use crate::cpu_blitter::CpuBlitter;
1389    use base64::Engine;
1390    use rlvgl_core::jpeg::Error as JpegError;
1391
1392    const RED_DOT_JPEG: &str = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDi6KKK+ZP3E//Z";
1393
1394    #[test]
1395    fn blitter_draws_jpeg() {
1396        let data = base64::engine::general_purpose::STANDARD
1397            .decode(RED_DOT_JPEG)
1398            .unwrap();
1399        let mut buf = [0u8; 4 * 4 * 4];
1400        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1401        let mut blit = CpuBlitter;
1402        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1403            BlitterRenderer::new(&mut blit, surface);
1404        renderer.draw_jpeg((0, 0), &data).unwrap();
1405        assert!(buf.iter().any(|&p| p != 0));
1406    }
1407
1408    #[test]
1409    fn blitter_rejects_invalid_jpeg() {
1410        let mut buf = [0u8; 4 * 4 * 4];
1411        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1412        let mut blit = CpuBlitter;
1413        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1414            BlitterRenderer::new(&mut blit, surface);
1415        let err = renderer.draw_jpeg((0, 0), b"not a jpeg").unwrap_err();
1416        assert!(matches!(err, JpegError::Format(_)));
1417    }
1418}
1419
1420#[cfg(all(test, feature = "gif"))]
1421mod gif_tests {
1422    use super::*;
1423    use crate::cpu_blitter::CpuBlitter;
1424    use base64::Engine;
1425    use rlvgl_core::gif::DecodingError as GifDecodingError;
1426
1427    const RED_DOT_GIF: &str = "R0lGODdhAQABAPAAAP8AAP///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
1428
1429    #[test]
1430    fn blitter_draws_gif() {
1431        let data = base64::engine::general_purpose::STANDARD
1432            .decode(RED_DOT_GIF)
1433            .unwrap();
1434        let mut buf = [0u8; 4 * 4 * 4];
1435        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1436        let mut blit = CpuBlitter;
1437        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1438            BlitterRenderer::new(&mut blit, surface);
1439        renderer.draw_gif_frame((0, 0), &data, 0).unwrap();
1440        assert!(buf.iter().any(|&p| p != 0));
1441    }
1442
1443    #[test]
1444    fn blitter_rejects_invalid_gif() {
1445        let mut buf = [0u8; 4 * 4 * 4];
1446        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1447        let mut blit = CpuBlitter;
1448        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1449            BlitterRenderer::new(&mut blit, surface);
1450        let err = renderer
1451            .draw_gif_frame((0, 0), b"not a gif", 0)
1452            .unwrap_err();
1453        assert!(matches!(err, GifDecodingError::Format(_)));
1454    }
1455}
1456
1457#[cfg(all(test, feature = "apng"))]
1458mod apng_tests {
1459    use super::*;
1460    use crate::cpu_blitter::CpuBlitter;
1461    use base64::Engine;
1462
1463    const RED_DOT_APNG: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACGFjVEwAAAABAAAAALQt6aAAAAAaZmNUTAAAAAAAAAABAAAAAQAAAAAAAAAAAGQD6AEAqmVSjAAAAA1JREFUeJxj+M/A8B8ABQAB/4mZPR0AAAAASUVORK5CYII=";
1464
1465    #[test]
1466    fn blitter_draws_apng() {
1467        let data = base64::engine::general_purpose::STANDARD
1468            .decode(RED_DOT_APNG)
1469            .unwrap();
1470        let mut buf = [0u8; 4 * 4 * 4];
1471        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1472        let mut blit = CpuBlitter;
1473        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1474            BlitterRenderer::new(&mut blit, surface);
1475        renderer.draw_apng_frame((0, 0), &data, 0).unwrap();
1476        assert!(buf.iter().any(|&p| p != 0));
1477    }
1478}
1479
1480#[cfg(all(test, feature = "canvas"))]
1481mod canvas_tests {
1482    use super::*;
1483    use crate::cpu_blitter::CpuBlitter;
1484    use embedded_graphics::prelude::Point;
1485    use rlvgl_core::canvas::Canvas;
1486
1487    #[test]
1488    fn blitter_draws_canvas() {
1489        let mut canvas = Canvas::new(1, 1);
1490        canvas.draw_pixel(Point::new(0, 0), Color(255, 0, 0, 255));
1491        let mut buf = [0u8; 4];
1492        let surface = Surface::new(&mut buf, 4, PixelFmt::Argb8888, 1, 1);
1493        let mut blit = CpuBlitter;
1494        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1495            BlitterRenderer::new(&mut blit, surface);
1496        renderer.draw_canvas((0, 0), &canvas);
1497        assert!(buf.iter().any(|&p| p != 0));
1498    }
1499}
1500
1501#[cfg(all(test, feature = "qrcode", not(target_os = "none")))]
1502mod qrcode_tests {
1503    use super::*;
1504    use crate::cpu_blitter::CpuBlitter;
1505    use rlvgl_core::qrcode::QrError;
1506
1507    #[test]
1508    fn blitter_draws_qr() {
1509        let mut buf = [0u8; 64 * 64 * 4];
1510        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1511        let mut blit = CpuBlitter;
1512        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1513            BlitterRenderer::new(&mut blit, surface);
1514        renderer.draw_qr((0, 0), b"hi").unwrap();
1515        assert!(buf.iter().any(|&p| p != 0));
1516    }
1517
1518    #[test]
1519    fn blitter_rejects_invalid_qr_data() {
1520        let mut buf = [0u8; 64 * 64 * 4];
1521        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1522        let mut blit = CpuBlitter;
1523        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1524            BlitterRenderer::new(&mut blit, surface);
1525        let data = vec![0u8; 3000];
1526        let err = renderer.draw_qr((0, 0), &data).unwrap_err();
1527        assert!(matches!(err, QrError::DataTooLong));
1528    }
1529}
1530
1531#[cfg(all(test, feature = "lottie"))]
1532mod lottie_tests {
1533    use super::*;
1534    use crate::cpu_blitter::CpuBlitter;
1535
1536    const SIMPLE_JSON: &str =
1537        "{\"v\":\"5.7\",\"fr\":30,\"ip\":0,\"op\":0,\"w\":1,\"h\":1,\"layers\":[]}";
1538
1539    #[test]
1540    fn blitter_draws_lottie() {
1541        let mut buf = [0u8; 4 * 4 * 4];
1542        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1543        let mut blit = CpuBlitter;
1544        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1545            BlitterRenderer::new(&mut blit, surface);
1546        renderer
1547            .draw_lottie_frame((0, 0), SIMPLE_JSON, 0, 1, 1)
1548            .unwrap();
1549        assert!(buf.iter().any(|&p| p != 0));
1550    }
1551
1552    #[test]
1553    fn blitter_rejects_invalid_lottie() {
1554        let mut buf = [0u8; 4 * 4 * 4];
1555        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1556        let mut blit = CpuBlitter;
1557        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1558            BlitterRenderer::new(&mut blit, surface);
1559        assert!(
1560            renderer
1561                .draw_lottie_frame((0, 0), "not json", 0, 1, 1)
1562                .is_err()
1563        );
1564    }
1565}
1566
1567#[cfg(all(test, feature = "pinyin", feature = "fontdue"))]
1568mod pinyin_tests {
1569    use super::*;
1570    use crate::cpu_blitter::CpuBlitter;
1571    use rlvgl_core::pinyin::PinyinInputMethod;
1572
1573    #[test]
1574    fn blitter_draws_pinyin() {
1575        let mut buf = [0u8; 64 * 64 * 4];
1576        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1577        let mut blit = CpuBlitter;
1578        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1579            BlitterRenderer::new(&mut blit, surface);
1580        let ime = PinyinInputMethod;
1581        assert!(renderer.draw_pinyin_candidates((0, 0), &ime, "zhong", Color(255, 255, 255, 255)));
1582        assert!(buf.iter().any(|&p| p != 0));
1583    }
1584
1585    #[test]
1586    fn pinyin_candidates_clipped_to_surface() {
1587        let mut buf = [0u8; 32 * 16 * 4];
1588        let surface = Surface::new(&mut buf, 32 * 4, PixelFmt::Argb8888, 32, 16);
1589        let mut blit = CpuBlitter;
1590        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1591            BlitterRenderer::new(&mut blit, surface);
1592        let ime = PinyinInputMethod;
1593        assert!(renderer.draw_pinyin_candidates((0, 0), &ime, "zhong", Color(255, 255, 255, 255)));
1594
1595        let mut expected = [0u8; 32 * 16 * 4];
1596        let surface_e = Surface::new(&mut expected, 32 * 4, PixelFmt::Argb8888, 32, 16);
1597        let mut blit_e = CpuBlitter;
1598        let mut renderer_e: BlitterRenderer<'_, CpuBlitter, 4> =
1599            BlitterRenderer::new(&mut blit_e, surface_e);
1600        let chars = ime.candidates("zhong").unwrap();
1601        let text: alloc::string::String = chars.into_iter().collect();
1602        let clipped: alloc::string::String = text.chars().take(2).collect();
1603        Renderer::draw_text(&mut renderer_e, (0, 0), &clipped, Color(255, 255, 255, 255));
1604        assert_eq!(buf[..], expected[..]);
1605    }
1606}
1607
1608#[cfg(all(test, feature = "fatfs", feature = "fontdue"))]
1609mod fatfs_tests {
1610    use super::*;
1611    use crate::cpu_blitter::CpuBlitter;
1612    use fatfs::{FileSystem, FormatVolumeOptions, FsOptions};
1613    use fscommon::BufStream;
1614    use std::io::{Cursor, Seek, SeekFrom, Write};
1615
1616    #[test]
1617    fn blitter_draws_fatfs_listing() {
1618        let mut img = Cursor::new(vec![0u8; 1024 * 512]);
1619        fatfs::format_volume(&mut img, FormatVolumeOptions::new()).unwrap();
1620        img.seek(SeekFrom::Start(0)).unwrap();
1621        {
1622            let buf_stream = BufStream::new(&mut img);
1623            let fs = FileSystem::new(buf_stream, FsOptions::new()).unwrap();
1624            fs.root_dir()
1625                .create_file("foo.txt")
1626                .unwrap()
1627                .write_all(b"hi")
1628                .unwrap();
1629        }
1630        img.seek(SeekFrom::Start(0)).unwrap();
1631        let mut buf = [0u8; 64 * 64 * 4];
1632        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1633        let mut blit = CpuBlitter;
1634        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1635            BlitterRenderer::new(&mut blit, surface);
1636        renderer
1637            .draw_fatfs_dir((0, 0), &mut img, "/", Color(255, 255, 255, 255))
1638            .unwrap();
1639        assert!(buf.iter().any(|&p| p != 0));
1640    }
1641
1642    #[test]
1643    fn fatfs_listing_clipped_to_surface() {
1644        let mut img = Cursor::new(vec![0u8; 1024 * 512]);
1645        fatfs::format_volume(&mut img, FormatVolumeOptions::new()).unwrap();
1646        img.seek(SeekFrom::Start(0)).unwrap();
1647        {
1648            let buf_stream = BufStream::new(&mut img);
1649            let fs = FileSystem::new(buf_stream, FsOptions::new()).unwrap();
1650            fs.root_dir().create_file("first_long_name.txt").unwrap();
1651            fs.root_dir().create_file("second_long_name.txt").unwrap();
1652            fs.root_dir().create_file("third_long_name.txt").unwrap();
1653        }
1654        img.seek(SeekFrom::Start(0)).unwrap();
1655        let image_vec = img.get_ref().clone();
1656        let mut img_expected = Cursor::new(image_vec.clone());
1657        let mut img_actual = Cursor::new(image_vec);
1658
1659        let mut buf = [0u8; 32 * 32 * 4];
1660        let surface = Surface::new(&mut buf, 32 * 4, PixelFmt::Argb8888, 32, 32);
1661        let mut blit = CpuBlitter;
1662        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1663            BlitterRenderer::new(&mut blit, surface);
1664        renderer
1665            .draw_fatfs_dir((0, 0), &mut img_actual, "/", Color(255, 255, 255, 255))
1666            .unwrap();
1667
1668        let names = rlvgl_core::fatfs::list_dir(&mut img_expected, "/").unwrap();
1669        let mut expected = [0u8; 32 * 32 * 4];
1670        let surface_e = Surface::new(&mut expected, 32 * 4, PixelFmt::Argb8888, 32, 32);
1671        let mut blit_e = CpuBlitter;
1672        let mut renderer_e: BlitterRenderer<'_, CpuBlitter, 4> =
1673            BlitterRenderer::new(&mut blit_e, surface_e);
1674        for (i, name) in names.iter().take(2).enumerate() {
1675            let clipped: alloc::string::String = name.chars().take(2).collect();
1676            Renderer::draw_text(
1677                &mut renderer_e,
1678                (0, (i as i32) * 16),
1679                &clipped,
1680                Color(255, 255, 255, 255),
1681            );
1682        }
1683        assert_eq!(buf[..], expected[..]);
1684    }
1685}
1686
1687#[cfg(all(test, feature = "nes"))]
1688mod nes_tests {
1689    use super::*;
1690    use crate::cpu_blitter::CpuBlitter;
1691
1692    #[test]
1693    fn blitter_draws_nes_frame() {
1694        let pixels = [Color(255, 0, 0, 255)];
1695        let mut buf = [0u8; 4];
1696        let surface = Surface::new(&mut buf, 4, PixelFmt::Argb8888, 1, 1);
1697        let mut blit = CpuBlitter;
1698        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1699            BlitterRenderer::new(&mut blit, surface);
1700        renderer.draw_nes_frame((0, 0), &pixels, 1, 1);
1701        assert!(buf.iter().any(|&p| p != 0));
1702    }
1703
1704    #[test]
1705    fn blitter_draws_full_nes_frame() {
1706        let mut pixels = [Color(0, 0, 0, 255); 256 * 240];
1707        for y in 0..240 {
1708            for x in 0..256 {
1709                pixels[y * 256 + x] = Color(x as u8, y as u8, 0, 255);
1710            }
1711        }
1712        let mut buf = [0u8; 256 * 240 * 4];
1713        let surface = Surface::new(&mut buf, 256 * 4, PixelFmt::Argb8888, 256, 240);
1714        let mut blit = CpuBlitter;
1715        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1716            BlitterRenderer::new(&mut blit, surface);
1717        renderer.draw_nes_frame((0, 0), &pixels, 256, 240);
1718        let x = 128usize;
1719        let y = 120usize;
1720        let idx = (y * 256 + x) * 4;
1721        let actual = u32::from_le_bytes(buf[idx..idx + 4].try_into().unwrap());
1722        let expected = Color(x as u8, y as u8, 0, 255).to_argb8888();
1723        assert_eq!(actual, expected);
1724    }
1725}