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