Skip to main content

kobo_core/rendering/
eink.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Nayeem Bin Ahsan
3//! E-ink framebuffer constants, MXCFB ioctl structs, and row-diff helper.
4//!
5//! The `Fb` struct (mmap + ioctl + `present()`) lives in
6//! [`crate::device::fb`]; it takes raw `&[u8]` RGB565 buffers, so it has no
7//! pixel-wrapper dependency.
8
9pub const FBIOGET_VSCREENINFO: libc::c_ulong = 0x4600;
10pub const FBIOGET_FSCREENINFO: libc::c_ulong = 0x4602;
11pub const MXCFB_SEND_UPDATE: libc::c_ulong = 0x4024462E;
12pub const MXCFB_WAIT_FOR_UPDATE_COMPLETE: libc::c_ulong = 0x4004462F;
13
14#[repr(C)]
15#[derive(Default)]
16pub struct FbVarScreeninfo {
17    pub xres: u32,
18    pub yres: u32,
19    pub xres_virtual: u32,
20    pub yres_virtual: u32,
21    pub xoffset: u32,
22    pub yoffset: u32,
23    pub bits_per_pixel: u32,
24    pub grayscale: u32,
25    pub rest: [u32; 32],
26}
27
28#[repr(C)]
29#[derive(Default)]
30pub struct FbFixScreeninfo {
31    id: [u8; 16],
32    pub smem_start: usize,
33    pub smem_len: u32,
34    typ: u32,
35    type_aux: u32,
36    visual: u32,
37    xpanstep: u16,
38    ypanstep: u16,
39    ywrapstep: u16,
40    pub line_length: u32,
41    rest: [u32; 16],
42}
43
44#[repr(C)]
45pub struct MxcfbRect {
46    pub top: u32,
47    pub left: u32,
48    pub width: u32,
49    pub height: u32,
50}
51
52#[repr(C)]
53pub struct MxcfbUpdateData {
54    pub update_region: MxcfbRect,
55    pub waveform_mode: u32,
56    pub update_mode: u32,
57    pub update_marker: u32,
58    pub temp: u32,
59    pub flags: u32,
60}
61
62pub const WAVE_INIT: u32 = 0;
63pub const WAVE_DU: u32 = 1;
64pub const WAVE_GC16: u32 = 2;
65pub const WAVE_GL16: u32 = 3;
66pub const WAVE_A2: u32 = 4;
67pub const WAVE_GLR16: u32 = 5;
68pub const WAVE_GLD16: u32 = 6;
69
70/// REAGL (GLR16) on MTK hwtcon devices (Libra/Clara Colour, mt8113).
71///
72/// The hwtcon enum maps 4 = GLR16/REAGL and 6 = A2, unlike the NXP mxcfb
73/// numbering the constants above follow -- so on MTK hardware [`WAVE_A2`]
74/// has in fact been driving REAGL. REAGL is the "full-quality, flash-free"
75/// waveform: paired with a FULL update it re-drives every pixel and applies
76/// regal ghost-suppression, clearing residue that GL16 leaves behind,
77/// without GC16's dark inversion blink. Use it for whole-screen interactive
78/// transitions on MTK devices.
79pub const WAVE_REAGL_MTK: u32 = 4;
80
81/// Pick the best waveform for a given render scenario on Kaleido 3 color e-ink.
82///
83/// - `Transition`: panel open/close, chapter overlay toggle. Needs good clearing
84///   to avoid ghosting, but no full flash. `GL16` keeps the swap flash-free;
85///   callers that need stronger clearing on a near-total swap use `WAVE_GC16`
86///   directly (still with a PARTIAL update -- only `update_mode=1` inverts).
87/// - `Content`: regular text page updates. `GL16` has less ghosting on partial
88///   updates than `GC16`, preserving color quality for highlighted text and
89///   the reading cursor.
90/// - `Animation`: spinner, loading bar. `A2` is fastest (monochrome).
91pub fn waveform_for(scenario: RenderScenario) -> u32 {
92    match scenario {
93        RenderScenario::Transition => WAVE_GL16,
94        RenderScenario::Content => WAVE_GL16,
95        RenderScenario::Animation => WAVE_A2,
96    }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum RenderScenario {
101    Transition,
102    Content,
103    Animation,
104}
105
106/// Compare two RGB565 byte buffers (same dimensions) and return the dirty row
107/// range aligned to 8-pixel boundaries. Returns `None` when buffers are
108/// identical.
109///
110/// `prev` and `cur` are raw byte views of `w * h` RGB565 pixels (2 bytes each,
111/// stride = `w * 2`). The caller is responsible for providing matching-length
112/// slices - typically via a `rgb565_as_bytes` helper on the pixel type.
113pub fn diff_rows(prev: &[u8], cur: &[u8], w: usize, h: usize) -> Option<(usize, usize)> {
114    let mut min_y = h;
115    let mut max_y = 0;
116    let mut dirty = false;
117    for y in 0..h {
118        let base = y * w * 2;
119        if prev[base..base + w * 2] != cur[base..base + w * 2] {
120            dirty = true;
121            if y < min_y {
122                min_y = y;
123            }
124            if y > max_y {
125                max_y = y;
126            }
127        }
128    }
129    if !dirty {
130        return None;
131    }
132    const A: usize = 8;
133    let top = (min_y / A) * A;
134    let bottom = ((max_y + A) / A) * A;
135    let rh = bottom.min(h) - top;
136    Some((top, rh))
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    fn make_buf(w: usize, h: usize, fill: u8) -> Vec<u8> {
144        vec![fill; w * h * 2]
145    }
146
147    #[test]
148    fn diff_rows_detects_change() {
149        let w = 10;
150        let h = 16;
151        let prev = make_buf(w, h, 0);
152        let mut cur = prev.clone();
153        let px = (5 * w + 3) * 2;
154        cur[px] = 1;
155        cur[px + 1] = 1;
156        let (top, rh) = diff_rows(&prev, &cur, w, h).unwrap();
157        assert_eq!(top, 0);
158        assert!(rh > 0);
159    }
160
161    #[test]
162    fn diff_rows_returns_none_when_identical() {
163        let w = 10;
164        let h = 10;
165        let buf = make_buf(w, h, 0);
166        assert!(diff_rows(&buf, &buf, w, h).is_none());
167    }
168
169    #[test]
170    fn diff_rows_aligns_to_8px_boundary() {
171        let w = 10;
172        let h = 32;
173        let prev = make_buf(w, h, 0);
174        let mut cur = prev.clone();
175        let px = (13 * w) * 2;
176        cur[px] = 1;
177        let (top, rh) = diff_rows(&prev, &cur, w, h).unwrap();
178        assert_eq!(top, 8);
179        assert_eq!(rh, 8);
180    }
181}