Skip to main content

rlvgl_platform/
cpu_blitter.rs

1//! CPU-based fallback blitter.
2//!
3//! Provides a pure software implementation of the [`Blitter`] trait used for
4//! testing and as a baseline on platforms lacking acceleration.
5
6use crate::blit::{BlitCaps, Blitter, PixelFmt, Rect, Surface};
7
8/// Blitter that performs all operations on the CPU using scalar loops.
9pub struct CpuBlitter;
10
11impl CpuBlitter {
12    fn pixel_size(fmt: PixelFmt) -> usize {
13        match fmt {
14            PixelFmt::Argb8888 => 4,
15            PixelFmt::Rgb565 => 2,
16            PixelFmt::L8 | PixelFmt::A8 => 1,
17            PixelFmt::A4 => 1,
18        }
19    }
20
21    fn argb8888_to_rgb565(c: u32) -> u16 {
22        let r = ((c >> 16) & 0xff) as u16;
23        let g = ((c >> 8) & 0xff) as u16;
24        let b = (c & 0xff) as u16;
25        ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
26    }
27
28    fn rgb565_to_argb8888(c: u16) -> u32 {
29        let r = ((c >> 11) & 0x1f) as u32;
30        let g = ((c >> 5) & 0x3f) as u32;
31        let b = (c & 0x1f) as u32;
32        0xff00_0000 | ((r << 3) << 16) | ((g << 2) << 8) | (b << 3)
33    }
34
35    fn read_pixel(surf: &Surface, x: i32, y: i32) -> u32 {
36        let bpp = Self::pixel_size(surf.format);
37        let offset = match surf.format {
38            PixelFmt::A4 => (y as usize * surf.stride) + (x as usize / 2),
39            _ => (y as usize * surf.stride) + (x as usize * bpp),
40        };
41        match surf.format {
42            PixelFmt::Argb8888 => {
43                let bytes: [u8; 4] = surf.buf[offset..offset + 4].try_into().unwrap();
44                u32::from_le_bytes(bytes)
45            }
46            PixelFmt::Rgb565 => {
47                let bytes: [u8; 2] = surf.buf[offset..offset + 2].try_into().unwrap();
48                Self::rgb565_to_argb8888(u16::from_le_bytes(bytes))
49            }
50            PixelFmt::L8 => {
51                let v = surf.buf[offset] as u32;
52                0xff00_0000 | (v << 16) | (v << 8) | v
53            }
54            PixelFmt::A8 => {
55                let a = surf.buf[offset] as u32;
56                a << 24
57            }
58            PixelFmt::A4 => {
59                let byte = surf.buf[offset];
60                let nib = if x & 1 == 0 { byte >> 4 } else { byte & 0x0f } as u32;
61                let a = (nib << 4) | nib;
62                a << 24
63            }
64        }
65    }
66
67    fn write_pixel(surf: &mut Surface, x: i32, y: i32, color: u32) {
68        let bpp = Self::pixel_size(surf.format);
69        let offset = match surf.format {
70            PixelFmt::A4 => (y as usize * surf.stride) + (x as usize / 2),
71            _ => (y as usize * surf.stride) + (x as usize * bpp),
72        };
73        match surf.format {
74            PixelFmt::Argb8888 => {
75                surf.buf[offset..offset + 4].copy_from_slice(&color.to_le_bytes());
76            }
77            PixelFmt::Rgb565 => {
78                let c = Self::argb8888_to_rgb565(color);
79                surf.buf[offset..offset + 2].copy_from_slice(&c.to_le_bytes());
80            }
81            PixelFmt::L8 => {
82                let r = (color >> 16) & 0xff;
83                let g = (color >> 8) & 0xff;
84                let b = color & 0xff;
85                surf.buf[offset] = ((r + g + b) / 3) as u8;
86            }
87            PixelFmt::A8 => {
88                surf.buf[offset] = (color >> 24) as u8;
89            }
90            PixelFmt::A4 => {
91                let a = (color >> 24) as u8 >> 4;
92                let byte = &mut surf.buf[offset];
93                if x & 1 == 0 {
94                    *byte = (*byte & 0x0f) | (a << 4);
95                } else {
96                    *byte = (*byte & 0xf0) | a;
97                }
98            }
99        }
100    }
101
102    fn blend_pixel(src: u32, dst: u32) -> u32 {
103        let sa = (src >> 24) & 0xff;
104        let inv = 255 - sa;
105        let sr = (src >> 16) & 0xff;
106        let sg = (src >> 8) & 0xff;
107        let sb = src & 0xff;
108        let dr = (dst >> 16) & 0xff;
109        let dg = (dst >> 8) & 0xff;
110        let db = dst & 0xff;
111        let r = (sr * sa + dr * inv) / 255;
112        let g = (sg * sa + dg * inv) / 255;
113        let b = (sb * sa + db * inv) / 255;
114        0xff00_0000 | (r << 16) | (g << 8) | b
115    }
116}
117
118impl Blitter for CpuBlitter {
119    fn caps(&self) -> BlitCaps {
120        BlitCaps::FILL | BlitCaps::BLIT | BlitCaps::BLEND | BlitCaps::PFC
121    }
122
123    fn fill(&mut self, dst: &mut Surface, area: Rect, color: u32) {
124        // Clamp to surface bounds to prevent out-of-bounds writes.
125        let sx = if area.x < 0 { 0i32 } else { area.x };
126        let sy = if area.y < 0 { 0i32 } else { area.y };
127        let ex = (area.x + area.w as i32).min(dst.width as i32);
128        let ey = (area.y + area.h as i32).min(dst.height as i32);
129        if sx >= ex || sy >= ey {
130            return;
131        }
132        let cw = (ex - sx) as u32;
133
134        match dst.format {
135            PixelFmt::Argb8888 => {
136                // 2026-05-17: replace per-pixel `chunks_exact_mut(4) +
137                // copy_from_slice` with a single `slice::fill` on a u32
138                // view. The old inner loop incurred a stack of ub_check
139                // intrinsics per pixel (`is_aligned_to`,
140                // `copy_nonoverlapping::precondition_check`,
141                // `maybe_is_nonoverlapping`, `from_raw_parts_mut`
142                // preconditions) in debug builds; PC-sampling on the
143                // disco-analyzer (2026-05-17) showed 95% of CM7 wall
144                // time inside this loop, pushing render to 0.435 Hz
145                // (~2.3 s per frame). slice::fill emits a single
146                // intrinsic that drops the per-pixel cost.
147                let bpp = 4usize;
148                for row in sy..ey {
149                    let start = row as usize * dst.stride + sx as usize * bpp;
150                    let end = start + cw as usize * bpp;
151                    if end > dst.buf.len() {
152                        break;
153                    }
154                    // SAFETY: `dst.buf[start..end]` is a contiguous
155                    // mutable byte slice owned by `dst` (Surface holds a
156                    // unique `&mut [u8]`). `start` is `row * stride + sx *
157                    // 4`; for Argb8888 surfaces stride is a multiple of 4
158                    // and sx*4 is naturally 4-aligned, so the cast to
159                    // `*mut u32` is sound. `end - start == cw * 4` so the
160                    // resulting `[u32]` has exactly `cw` elements. No
161                    // aliasing: we have unique access for this match arm;
162                    // no ISR touches `dst.buf` (Surface is single-writer
163                    // by construction).
164                    let line = &mut dst.buf[start..end];
165                    let line_u32 = unsafe {
166                        core::slice::from_raw_parts_mut(
167                            line.as_mut_ptr().cast::<u32>(),
168                            cw as usize,
169                        )
170                    };
171                    line_u32.fill(color.to_le());
172                }
173            }
174            PixelFmt::Rgb565 => {
175                let c = Self::argb8888_to_rgb565(color);
176                let bpp = 2usize;
177                for row in sy..ey {
178                    let start = row as usize * dst.stride + sx as usize * bpp;
179                    let end = start + cw as usize * bpp;
180                    if end > dst.buf.len() {
181                        break;
182                    }
183                    // SAFETY: same provenance argument as the Argb8888
184                    // branch above, with `*mut u16` instead. `start` is
185                    // `row * stride + sx * 2`; Rgb565 surfaces have a
186                    // 2-aligned stride and sx*2 is 2-aligned.
187                    let line = &mut dst.buf[start..end];
188                    let line_u16 = unsafe {
189                        core::slice::from_raw_parts_mut(
190                            line.as_mut_ptr().cast::<u16>(),
191                            cw as usize,
192                        )
193                    };
194                    line_u16.fill(c.to_le());
195                }
196            }
197            _ => {
198                for y in sy..ey {
199                    for x in sx..ex {
200                        Self::write_pixel(dst, x, y, color);
201                    }
202                }
203            }
204        }
205    }
206
207    fn blit(&mut self, src: &Surface, src_area: Rect, dst: &mut Surface, dst_pos: (i32, i32)) {
208        // Clip source rect to destination bounds.
209        let clip_x = if dst_pos.0 < 0 { -dst_pos.0 } else { 0 };
210        let clip_y = if dst_pos.1 < 0 { -dst_pos.1 } else { 0 };
211        let dst_x = dst_pos.0.max(0);
212        let dst_y = dst_pos.1.max(0);
213        let w = (src_area.w as i32 - clip_x).min(dst.width as i32 - dst_x);
214        let h = (src_area.h as i32 - clip_y).min(dst.height as i32 - dst_y);
215        if w <= 0 || h <= 0 {
216            return;
217        }
218
219        let src_x0 = src_area.x + clip_x;
220        let src_y0 = src_area.y + clip_y;
221
222        if src.format == dst.format {
223            let bpp = Self::pixel_size(src.format);
224            for row in 0..h {
225                let src_start = ((src_y0 + row) as usize * src.stride) + (src_x0 as usize * bpp);
226                let dst_start = ((dst_y + row) as usize * dst.stride) + (dst_x as usize * bpp);
227                let len = w as usize * bpp;
228                dst.buf[dst_start..dst_start + len]
229                    .copy_from_slice(&src.buf[src_start..src_start + len]);
230            }
231            return;
232        }
233
234        for row in 0..h {
235            for col in 0..w {
236                let px = Self::read_pixel(src, src_x0 + col, src_y0 + row);
237                Self::write_pixel(dst, dst_x + col, dst_y + row, px);
238            }
239        }
240    }
241
242    fn blend(&mut self, src: &Surface, src_area: Rect, dst: &mut Surface, dst_pos: (i32, i32)) {
243        // Clip source rect to destination bounds.
244        let clip_x = if dst_pos.0 < 0 { -dst_pos.0 } else { 0 };
245        let clip_y = if dst_pos.1 < 0 { -dst_pos.1 } else { 0 };
246        let dst_x = dst_pos.0.max(0);
247        let dst_y = dst_pos.1.max(0);
248        let w = (src_area.w as i32 - clip_x).min(dst.width as i32 - dst_x);
249        let h = (src_area.h as i32 - clip_y).min(dst.height as i32 - dst_y);
250        if w <= 0 || h <= 0 {
251            return;
252        }
253
254        let src_x0 = src_area.x + clip_x;
255        let src_y0 = src_area.y + clip_y;
256
257        for row in 0..h {
258            for col in 0..w {
259                let s = Self::read_pixel(src, src_x0 + col, src_y0 + row);
260                let d = Self::read_pixel(dst, dst_x + col, dst_y + row);
261                let out = Self::blend_pixel(s, d);
262                Self::write_pixel(dst, dst_x + col, dst_y + row, out);
263            }
264        }
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use heapless::Vec;
272
273    #[test]
274    fn fill_argb8888() {
275        let mut buf = [0u8; 64];
276        let mut surf = Surface::new(&mut buf, 16, PixelFmt::Argb8888, 4, 4);
277        let mut blit = CpuBlitter;
278        blit.fill(
279            &mut surf,
280            Rect {
281                x: 0,
282                y: 0,
283                w: 4,
284                h: 4,
285            },
286            0x11223344,
287        );
288        for px in buf.chunks_exact(4) {
289            assert_eq!(u32::from_le_bytes(px.try_into().unwrap()), 0x11223344);
290        }
291    }
292
293    #[test]
294    fn blit_argb8888_to_rgb565() {
295        let mut src_buf = [0u8; 16];
296        let mut dst_buf = [0u8; 32];
297        let src_colors = [0xff0000ffu32, 0xff00ff00, 0xffff0000, 0xffffffff];
298        for (i, chunk) in src_buf.chunks_exact_mut(4).enumerate() {
299            chunk.copy_from_slice(&src_colors[i].to_le_bytes());
300        }
301        let src = Surface::new(&mut src_buf, 8, PixelFmt::Argb8888, 2, 2);
302        let mut dst = Surface::new(&mut dst_buf, 8, PixelFmt::Rgb565, 4, 4);
303        let mut blit = CpuBlitter;
304        blit.blit(
305            &src,
306            Rect {
307                x: 0,
308                y: 0,
309                w: 2,
310                h: 2,
311            },
312            &mut dst,
313            (1, 1),
314        );
315        let expected: Vec<u16, 4> = Vec::from_slice(&[
316            CpuBlitter::argb8888_to_rgb565(0xff0000ff),
317            CpuBlitter::argb8888_to_rgb565(0xff00ff00),
318            CpuBlitter::argb8888_to_rgb565(0xffff0000),
319            CpuBlitter::argb8888_to_rgb565(0xffffffff),
320        ])
321        .unwrap();
322        for (i, row) in (1..3).enumerate() {
323            for (j, col) in (1..3).enumerate() {
324                let idx = row * 8 + col * 2;
325                let val = u16::from_le_bytes([dst_buf[idx], dst_buf[idx + 1]]);
326                assert_eq!(val, expected[i * 2 + j]);
327            }
328        }
329    }
330
331    #[test]
332    fn blend_argb8888() {
333        let mut src_buf = [0u8; 16];
334        let mut dst_buf = [0u8; 16];
335        for chunk in src_buf.chunks_exact_mut(4) {
336            chunk.copy_from_slice(&0x80ff0000u32.to_le_bytes());
337        }
338        for chunk in dst_buf.chunks_exact_mut(4) {
339            chunk.copy_from_slice(&0xff000000u32.to_le_bytes());
340        }
341        let src = Surface::new(&mut src_buf, 8, PixelFmt::Argb8888, 2, 2);
342        let mut dst = Surface::new(&mut dst_buf, 8, PixelFmt::Argb8888, 2, 2);
343        let mut blit = CpuBlitter;
344        blit.blend(
345            &src,
346            Rect {
347                x: 0,
348                y: 0,
349                w: 2,
350                h: 2,
351            },
352            &mut dst,
353            (0, 0),
354        );
355        for chunk in dst_buf.chunks_exact(4) {
356            assert_eq!(u32::from_le_bytes(chunk.try_into().unwrap()), 0xff800000);
357        }
358    }
359}