Skip to main content

kobo_core/device/
fb.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Nayeem Bin Ahsan
3//! Framebuffer: mmap'd `/dev/fb0` with MXCFB e-ink refresh.
4//!
5//! Takes raw `&[u8]` RGB565 buffers (2 bytes/pixel, little-endian) so the
6//! caller is not locked into a specific pixel wrapper type (slint's
7//! `Rgb565Pixel`, a plain `u16` newtype, etc.). The app crate provides a
8//! `rgb565_as_bytes_ref` helper to convert from `&[Rgb565Pixel]` at the
9//! call site.
10
11use crate::rendering::eink::{
12    FbFixScreeninfo, FbVarScreeninfo, MxcfbRect, MxcfbUpdateData, FBIOGET_FSCREENINFO,
13    FBIOGET_VSCREENINFO, MXCFB_SEND_UPDATE,
14};
15use log::{info, warn};
16
17const UPDATE_MARKER: u32 = 1;
18const WAIT_FALLBACK_MS: u64 = 400;
19
20#[derive(Debug, Clone, Copy)]
21pub struct UpdateRegion {
22    pub x: usize,
23    pub y: usize,
24    pub w: usize,
25    pub h: usize,
26}
27
28pub fn dump_ppm(path: &str, buf: &[u8], w: usize, h: usize) {
29    let mut out = Vec::with_capacity(15 + w * h * 3);
30    out.extend_from_slice(format!("P6\n{} {}\n255\n", w, h).as_bytes());
31    for i in 0..w * h {
32        let off = i * 2;
33        let v = (buf[off] as u16) | ((buf[off + 1] as u16) << 8);
34        let r = ((v >> 11) & 0x1f) as u8;
35        let g = ((v >> 5) & 0x3f) as u8;
36        let b = (v & 0x1f) as u8;
37        out.push((r << 3) | (r >> 2));
38        out.push((g << 2) | (g >> 4));
39        out.push((b << 3) | (b >> 2));
40    }
41    match std::fs::write(path, &out) {
42        Ok(_) => (),
43        Err(e) => warn!("PPM write err: {}", e),
44    }
45}
46
47pub struct Fb {
48    _file: std::fs::File,
49    pub fd: libc::c_int,
50    pub ptr: *mut u8,
51    pub map_len: usize,
52    pub stride: usize,
53    pub bpp: usize,
54    pub xres: usize,
55    pub yres: usize,
56    r_off: u32,
57    g_off: u32,
58    b_off: u32,
59}
60
61impl Fb {
62    pub fn open() -> Option<Fb> {
63        use std::os::unix::io::AsRawFd;
64        let file = std::fs::OpenOptions::new()
65            .read(true)
66            .write(true)
67            .open("/dev/fb0")
68            .ok()?;
69        let fd = file.as_raw_fd();
70        let mut var = FbVarScreeninfo::default();
71        let mut fix = FbFixScreeninfo::default();
72        // SAFETY: fd is a valid /dev/fb0 descriptor (file alive on this frame). FBIOGET_* read
73        // one fb_var/fb_fix screeninfo into the supplied &mut; both are #[repr(C)] structs of
74        // the kernel-expected layout, exclusively borrowed. A failing ioctl returns <0 and we
75        // bail; on success the structs are fully overwritten with valid values.
76        unsafe {
77            if libc::ioctl(fd, FBIOGET_VSCREENINFO as _, &mut var) < 0 {
78                return None;
79            }
80            if libc::ioctl(fd, FBIOGET_FSCREENINFO as _, &mut fix) < 0 {
81                return None;
82            }
83        }
84        let map_len = fix.smem_len as usize;
85        // SAFETY: mmap of the framebuffer with MAP_SHARED over the valid fd. map_len comes
86        // straight from the kernel's fix.smem_len. NULL addr = kernel picks. We check for
87        // MAP_FAILED immediately and bail; the returned pointer is the device-backed mapping
88        // owned by Fb (unmapped in Drop).
89        let ptr = unsafe {
90            libc::mmap(
91                std::ptr::null_mut(),
92                map_len,
93                libc::PROT_READ | libc::PROT_WRITE,
94                libc::MAP_SHARED,
95                fd,
96                0,
97            )
98        };
99        if ptr == libc::MAP_FAILED {
100            return None;
101        }
102        info!(
103            "fb: {}x{} bpp={} line_length={} smem_len={}",
104            var.xres, var.yres, var.bits_per_pixel, fix.line_length, fix.smem_len
105        );
106        let (r_off, g_off, b_off) = (
107            var.rest.first().copied().unwrap_or(16),
108            var.rest.get(3).copied().unwrap_or(8),
109            var.rest.get(6).copied().unwrap_or(0),
110        );
111        info!("fb: rgb offsets r={} g={} b={}", r_off, g_off, b_off);
112        Some(Fb {
113            _file: file,
114            fd,
115            ptr: ptr as *mut u8,
116            map_len,
117            stride: fix.line_length as usize,
118            bpp: var.bits_per_pixel as usize,
119            xres: var.xres as usize,
120            yres: var.yres as usize,
121            r_off,
122            g_off,
123            b_off,
124        })
125    }
126
127    /// Blit and refresh an arbitrary rectangle, rather than `present`'s
128    /// full-width band.
129    ///
130    /// Needed when a waveform must be kept off neighbouring pixels: a 1-bit
131    /// waveform (A2/DU) drives every pixel in its update region to black or
132    /// white, so a full-width band would flatten anything colourful sharing those
133    /// rows. Bounding the region horizontally keeps the waveform on the animated
134    /// area alone.
135    pub fn present_rect(&self, buf: &[u8], w: usize, h: usize, rect: &UpdateRegion, waveform: u32) {
136        let x0 = rect.x.min(self.xres);
137        let y0 = rect.y.min(self.yres);
138        let x1 = (rect.x + rect.w).min(w).min(self.xres);
139        let y1 = (rect.y + rect.h).min(h).min(self.yres);
140        if x0 >= x1 || y0 >= y1 {
141            return;
142        }
143        // SAFETY: identical to `present` -- self.ptr is the mmap'd framebuffer of
144        // length self.map_len, and we only write fb pixels through this &self.
145        let fb = unsafe { std::slice::from_raw_parts_mut(self.ptr, self.map_len) };
146        let bpp = self.bpp;
147        let stride = self.stride;
148        for py in y0..y1 {
149            let row = py * w;
150            let fb_row = py * stride;
151            for px in x0..x1 {
152                let buf_off = (row + px) * 2;
153                let pix = (buf[buf_off] as u16) | ((buf[buf_off + 1] as u16) << 8);
154                let off = fb_row + px * (bpp / 8);
155                match bpp {
156                    32 => {
157                        let r = (((pix >> 11) & 0x1f) << 3) as u8;
158                        let g = (((pix >> 5) & 0x3f) << 2) as u8;
159                        let b = ((pix & 0x1f) << 3) as u8;
160                        fb[off + (self.r_off / 8) as usize] = r;
161                        fb[off + (self.g_off / 8) as usize] = g;
162                        fb[off + (self.b_off / 8) as usize] = b;
163                        fb[off + 3] = 0xff;
164                    }
165                    16 => {
166                        fb[off] = (pix & 0xff) as u8;
167                        fb[off + 1] = (pix >> 8) as u8;
168                    }
169                    _ => {}
170                }
171            }
172        }
173        // msync whole rows spanning the rect: page granularity makes a tighter
174        // flush pointless, and MS_SYNC on the row span is what the ioctl needs.
175        let sync_start = y0 * stride;
176        let sync_len = ((y1 - y0) * stride).max(1);
177        // SAFETY: sync_start + sync_len <= yres*stride <= map_len (y1 clamped to
178        // yres above), so the range stays inside the mapping.
179        unsafe {
180            libc::msync(
181                self.ptr.add(sync_start) as *mut libc::c_void,
182                sync_len,
183                libc::MS_SYNC,
184            );
185        }
186        let upd = MxcfbUpdateData {
187            update_region: MxcfbRect {
188                top: y0 as u32,
189                left: x0 as u32,
190                width: (x1 - x0) as u32,
191                height: (y1 - y0) as u32,
192            },
193            waveform_mode: waveform,
194            update_mode: 0,
195            update_marker: UPDATE_MARKER,
196            temp: 0x1000,
197            flags: 0,
198        };
199        // SAFETY: MXCFB_SEND_UPDATE reads one initialized #[repr(C)]
200        // MxcfbUpdateData; self.fd is the valid fb0 descriptor. rc<0 is non-fatal.
201        let _ = unsafe { libc::ioctl(self.fd, MXCFB_SEND_UPDATE as _, &upd) };
202    }
203
204    /// Blit an RGB565 byte buffer to the framebuffer and trigger an e-ink
205    /// refresh. `buf` is `w * h * 2` bytes, little-endian RGB565.
206    pub fn present(
207        &self,
208        buf: &[u8],
209        w: usize,
210        h: usize,
211        full: bool,
212        top: usize,
213        rh: usize,
214        waveform: u32,
215    ) {
216        // SAFETY: self.ptr is the mmap'd framebuffer of length self.map_len (set in open(),
217        // unmapped in Drop). We hold &self (shared) but only write fb pixels here - no other
218        // aliasing byte slice of this mapping is live concurrently. The slice length is exactly
219        // map_len, matching the mapping.
220        let fb = unsafe { std::slice::from_raw_parts_mut(self.ptr, self.map_len) };
221        let bpp = self.bpp;
222        let stride = self.stride;
223        let (y0, y1) = if full && top == 0 && rh == 0 {
224            (0, h.min(self.yres))
225        } else {
226            let end = (top + rh).min(h).min(self.yres);
227            (top.min(end), end)
228        };
229        let x1 = w.min(self.xres);
230        for y in y0..y1 {
231            let row = y * w;
232            let fb_row = y * stride;
233            for x in 0..x1 {
234                let buf_off = (row + x) * 2;
235                let px = (buf[buf_off] as u16) | ((buf[buf_off + 1] as u16) << 8);
236                let off = fb_row + x * (bpp / 8);
237                match bpp {
238                    32 => {
239                        let r = (((px >> 11) & 0x1f) << 3) as u8;
240                        let g = (((px >> 5) & 0x3f) << 2) as u8;
241                        let b = ((px & 0x1f) << 3) as u8;
242                        fb[off + (self.r_off / 8) as usize] = r;
243                        fb[off + (self.g_off / 8) as usize] = g;
244                        fb[off + (self.b_off / 8) as usize] = b;
245                        fb[off + 3] = 0xff;
246                    }
247                    16 => {
248                        fb[off] = (px & 0xff) as u8;
249                        fb[off + 1] = (px >> 8) as u8;
250                    }
251                    _ => {}
252                }
253            }
254        }
255        let sync_start = y0 * self.stride;
256        let sync_len = ((y1 - y0) * self.stride).max(1);
257        // SAFETY: self.ptr.add(sync_start) stays within [self.ptr, self.ptr+map_len) because
258        // sync_start = y0*stride and sync_len = (y1-y0)*stride, with y0/y1 clamped to yres and
259        // stride*xres*... <= map_len for a linear framebuffer. MS_SYNC flushes the dirty pages.
260        unsafe {
261            libc::msync(
262                self.ptr.add(sync_start) as *mut libc::c_void,
263                sync_len,
264                libc::MS_SYNC,
265            );
266        }
267        let upd = MxcfbUpdateData {
268            update_region: MxcfbRect {
269                top: y0 as u32,
270                left: 0,
271                width: x1 as u32,
272                height: (y1 - y0) as u32,
273            },
274            waveform_mode: waveform,
275            update_mode: if full { 1 } else { 0 },
276            update_marker: UPDATE_MARKER,
277            temp: 0x1000,
278            flags: 0,
279        };
280        // SAFETY: MXCFB_SEND_UPDATE ioctl reads one #[repr(C)] MxcfbUpdateData from the &upd
281        // pointer (fully initialized above) to schedule the e-ink refresh. self.fd is the
282        // valid fb0 descriptor; a failing ioctl returns <0 (logged) and is non-fatal.
283        let _ = unsafe { libc::ioctl(self.fd, MXCFB_SEND_UPDATE as _, &upd) };
284    }
285
286    pub fn wait_for_update_complete(&self) {
287        let marker: u32 = UPDATE_MARKER;
288        // SAFETY: self.fd is the valid fb0 descriptor opened in Fb::open; marker is a stack u32
289        // passed by const ptr, read-once by the ioctl to match the update_marker from present().
290        let rc = unsafe {
291            libc::ioctl(
292                self.fd,
293                crate::rendering::eink::MXCFB_WAIT_FOR_UPDATE_COMPLETE as _,
294                &marker as *const u32,
295            )
296        };
297        if rc < 0 {
298            std::thread::sleep(std::time::Duration::from_millis(WAIT_FALLBACK_MS));
299        }
300    }
301}
302
303impl Drop for Fb {
304    fn drop(&mut self) {
305        // SAFETY: self.ptr/self.map_len describe the single mmap acquired in open(); Drop runs
306        // once, no other reference to the mapping exists (file dropped right after), so
307        // munmap is sound and releases the device mapping.
308        unsafe {
309            libc::munmap(self.ptr as *mut libc::c_void, self.map_len);
310        }
311    }
312}