Skip to main content

kobo_core/device/
fb.rs

1//! Framebuffer: mmap'd `/dev/fb0` with MXCFB e-ink refresh.
2//!
3//! Takes raw `&[u8]` RGB565 buffers (2 bytes/pixel, little-endian) so the
4//! caller is not locked into a specific pixel wrapper type (slint's
5//! `Rgb565Pixel`, a plain `u16` newtype, etc.). The app crate provides a
6//! `rgb565_as_bytes_ref` helper to convert from `&[Rgb565Pixel]` at the
7//! call site.
8
9use crate::rendering::eink::{
10    FbFixScreeninfo, FbVarScreeninfo, MxcfbRect, MxcfbUpdateData, FBIOGET_FSCREENINFO,
11    FBIOGET_VSCREENINFO, MXCFB_SEND_UPDATE,
12};
13use log::{debug, info, warn};
14
15pub fn dump_ppm(path: &str, buf: &[u8], w: usize, h: usize) {
16    let mut out = Vec::with_capacity(15 + w * h * 3);
17    out.extend_from_slice(format!("P6\n{} {}\n255\n", w, h).as_bytes());
18    for i in 0..w * h {
19        let off = i * 2;
20        let v = (buf[off] as u16) | ((buf[off + 1] as u16) << 8);
21        let r = ((v >> 11) & 0x1f) as u8;
22        let g = ((v >> 5) & 0x3f) as u8;
23        let b = (v & 0x1f) as u8;
24        out.push((r << 3) | (r >> 2));
25        out.push((g << 2) | (g >> 4));
26        out.push((b << 3) | (b >> 2));
27    }
28    match std::fs::write(path, &out) {
29        Ok(_) => debug!("wrote {} ({} bytes)", path, out.len()),
30        Err(e) => warn!("PPM write err: {}", e),
31    }
32}
33
34pub struct Fb {
35    _file: std::fs::File,
36    pub fd: libc::c_int,
37    pub ptr: *mut u8,
38    pub map_len: usize,
39    pub stride: usize,
40    pub bpp: usize,
41    pub xres: usize,
42    pub yres: usize,
43    r_off: u32,
44    g_off: u32,
45    b_off: u32,
46}
47
48impl Fb {
49    pub fn open() -> Option<Fb> {
50        use std::os::unix::io::AsRawFd;
51        let file = std::fs::OpenOptions::new()
52            .read(true)
53            .write(true)
54            .open("/dev/fb0")
55            .ok()?;
56        let fd = file.as_raw_fd();
57        let mut var = FbVarScreeninfo::default();
58        let mut fix = FbFixScreeninfo::default();
59        // SAFETY: fd is a valid /dev/fb0 descriptor (file alive on this frame). FBIOGET_* read
60        // one fb_var/fb_fix screeninfo into the supplied &mut; both are #[repr(C)] structs of
61        // the kernel-expected layout, exclusively borrowed. A failing ioctl returns <0 and we
62        // bail; on success the structs are fully overwritten with valid values.
63        unsafe {
64            if libc::ioctl(fd, FBIOGET_VSCREENINFO as _, &mut var) < 0 {
65                return None;
66            }
67            if libc::ioctl(fd, FBIOGET_FSCREENINFO as _, &mut fix) < 0 {
68                return None;
69            }
70        }
71        let map_len = fix.smem_len as usize;
72        // SAFETY: mmap of the framebuffer with MAP_SHARED over the valid fd. map_len comes
73        // straight from the kernel's fix.smem_len. NULL addr = kernel picks. We check for
74        // MAP_FAILED immediately and bail; the returned pointer is the device-backed mapping
75        // owned by Fb (unmapped in Drop).
76        let ptr = unsafe {
77            libc::mmap(
78                std::ptr::null_mut(),
79                map_len,
80                libc::PROT_READ | libc::PROT_WRITE,
81                libc::MAP_SHARED,
82                fd,
83                0,
84            )
85        };
86        if ptr == libc::MAP_FAILED {
87            return None;
88        }
89        info!(
90            "fb: {}x{} bpp={} line_length={} smem_len={}",
91            var.xres, var.yres, var.bits_per_pixel, fix.line_length, fix.smem_len
92        );
93        let (r_off, g_off, b_off) = (
94            var.rest.first().copied().unwrap_or(16),
95            var.rest.get(3).copied().unwrap_or(8),
96            var.rest.get(6).copied().unwrap_or(0),
97        );
98        info!("fb: rgb offsets r={} g={} b={}", r_off, g_off, b_off);
99        Some(Fb {
100            _file: file,
101            fd,
102            ptr: ptr as *mut u8,
103            map_len,
104            stride: fix.line_length as usize,
105            bpp: var.bits_per_pixel as usize,
106            xres: var.xres as usize,
107            yres: var.yres as usize,
108            r_off,
109            g_off,
110            b_off,
111        })
112    }
113
114    /// Blit an RGB565 byte buffer to the framebuffer and trigger an e-ink
115    /// refresh. `buf` is `w * h * 2` bytes, little-endian RGB565.
116    pub fn present(
117        &self,
118        buf: &[u8],
119        w: usize,
120        h: usize,
121        full: bool,
122        top: usize,
123        rh: usize,
124        waveform: u32,
125    ) {
126        // SAFETY: self.ptr is the mmap'd framebuffer of length self.map_len (set in open(),
127        // unmapped in Drop). We hold &self (shared) but only write fb pixels here - no other
128        // aliasing byte slice of this mapping is live concurrently. The slice length is exactly
129        // map_len, matching the mapping.
130        let fb = unsafe { std::slice::from_raw_parts_mut(self.ptr, self.map_len) };
131        let bpp = self.bpp;
132        let stride = self.stride;
133        let (y0, y1) = if full {
134            (0, h.min(self.yres))
135        } else {
136            let end = (top + rh).min(h).min(self.yres);
137            (top.min(end), end)
138        };
139        let x1 = w.min(self.xres);
140        for y in y0..y1 {
141            let row = y * w;
142            let fb_row = y * stride;
143            for x in 0..x1 {
144                let buf_off = (row + x) * 2;
145                let px = (buf[buf_off] as u16) | ((buf[buf_off + 1] as u16) << 8);
146                let off = fb_row + x * (bpp / 8);
147                match bpp {
148                    32 => {
149                        let r = (((px >> 11) & 0x1f) << 3) as u8;
150                        let g = (((px >> 5) & 0x3f) << 2) as u8;
151                        let b = ((px & 0x1f) << 3) as u8;
152                        fb[off + (self.r_off / 8) as usize] = r;
153                        fb[off + (self.g_off / 8) as usize] = g;
154                        fb[off + (self.b_off / 8) as usize] = b;
155                        fb[off + 3] = 0xff;
156                    }
157                    16 => {
158                        fb[off] = (px & 0xff) as u8;
159                        fb[off + 1] = (px >> 8) as u8;
160                    }
161                    _ => {}
162                }
163            }
164        }
165        let sync_start = y0 * self.stride;
166        let sync_len = ((y1 - y0) * self.stride).max(1);
167        // SAFETY: self.ptr.add(sync_start) stays within [self.ptr, self.ptr+map_len) because
168        // sync_start = y0*stride and sync_len = (y1-y0)*stride, with y0/y1 clamped to yres and
169        // stride*xres*... <= map_len for a linear framebuffer. MS_SYNC flushes the dirty pages.
170        unsafe {
171            libc::msync(
172                self.ptr.add(sync_start) as *mut libc::c_void,
173                sync_len,
174                libc::MS_SYNC,
175            );
176        }
177        let upd = MxcfbUpdateData {
178            update_region: MxcfbRect {
179                top: y0 as u32,
180                left: 0,
181                width: x1 as u32,
182                height: (y1 - y0) as u32,
183            },
184            waveform_mode: waveform,
185            update_mode: if full { 1 } else { 0 },
186            update_marker: 1,
187            temp: 0x1000,
188            flags: 0,
189        };
190        // SAFETY: MXCFB_SEND_UPDATE ioctl reads one #[repr(C)] MxcfbUpdateData from the &upd
191        // pointer (fully initialized above) to schedule the e-ink refresh. self.fd is the
192        // valid fb0 descriptor; a failing ioctl returns <0 (logged) and is non-fatal.
193        let rc = unsafe { libc::ioctl(self.fd, MXCFB_SEND_UPDATE as _, &upd) };
194        debug!(
195            "eink refresh (GC16 {} rows=[{},{}] {}x{}) rc={}",
196            if full { "FULL" } else { "PARTIAL" },
197            y0,
198            y1,
199            x1,
200            y1 - y0,
201            rc
202        );
203    }
204}
205
206impl Drop for Fb {
207    fn drop(&mut self) {
208        // SAFETY: self.ptr/self.map_len describe the single mmap acquired in open(); Drop runs
209        // once, no other reference to the mapping exists (file dropped right after), so
210        // munmap is sound and releases the device mapping.
211        unsafe {
212            libc::munmap(self.ptr as *mut libc::c_void, self.map_len);
213        }
214    }
215}