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/// Pick the best waveform for a given render scenario on Kaleido 3 color e-ink.
71///
72/// - `Transition`: panel open/close, chapter overlay toggle. Needs good clearing
73///   to avoid ghosting, but no full flash. `GC16` partial clears better than
74///   `GL16` for large-area changes.
75/// - `Content`: regular text page updates. `GL16` has less ghosting on partial
76///   updates than `GC16`, preserving color quality for highlighted text and
77///   the reading cursor.
78/// - `Animation`: spinner, loading bar. `A2` is fastest (monochrome).
79pub fn waveform_for(scenario: RenderScenario) -> u32 {
80    match scenario {
81        RenderScenario::Transition => WAVE_GL16,
82        RenderScenario::Content => WAVE_GL16,
83        RenderScenario::Animation => WAVE_A2,
84    }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum RenderScenario {
89    Transition,
90    Content,
91    Animation,
92}
93
94/// Compare two RGB565 byte buffers (same dimensions) and return the dirty row
95/// range aligned to 8-pixel boundaries. Returns `None` when buffers are
96/// identical.
97///
98/// `prev` and `cur` are raw byte views of `w * h` RGB565 pixels (2 bytes each,
99/// stride = `w * 2`). The caller is responsible for providing matching-length
100/// slices - typically via a `rgb565_as_bytes` helper on the pixel type.
101pub fn diff_rows(prev: &[u8], cur: &[u8], w: usize, h: usize) -> Option<(usize, usize)> {
102    let mut min_y = h;
103    let mut max_y = 0;
104    let mut dirty = false;
105    for y in 0..h {
106        let base = y * w * 2;
107        if prev[base..base + w * 2] != cur[base..base + w * 2] {
108            dirty = true;
109            if y < min_y {
110                min_y = y;
111            }
112            if y > max_y {
113                max_y = y;
114            }
115        }
116    }
117    if !dirty {
118        return None;
119    }
120    const A: usize = 8;
121    let top = (min_y / A) * A;
122    let bottom = ((max_y + A) / A) * A;
123    let rh = bottom.min(h) - top;
124    Some((top, rh))
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    fn make_buf(w: usize, h: usize, fill: u8) -> Vec<u8> {
132        vec![fill; w * h * 2]
133    }
134
135    #[test]
136    fn diff_rows_detects_change() {
137        let w = 10;
138        let h = 16;
139        let prev = make_buf(w, h, 0);
140        let mut cur = prev.clone();
141        let px = (5 * w + 3) * 2;
142        cur[px] = 1;
143        cur[px + 1] = 1;
144        let (top, rh) = diff_rows(&prev, &cur, w, h).unwrap();
145        assert_eq!(top, 0);
146        assert!(rh > 0);
147    }
148
149    #[test]
150    fn diff_rows_returns_none_when_identical() {
151        let w = 10;
152        let h = 10;
153        let buf = make_buf(w, h, 0);
154        assert!(diff_rows(&buf, &buf, w, h).is_none());
155    }
156
157    #[test]
158    fn diff_rows_aligns_to_8px_boundary() {
159        let w = 10;
160        let h = 32;
161        let prev = make_buf(w, h, 0);
162        let mut cur = prev.clone();
163        let px = (13 * w) * 2;
164        cur[px] = 1;
165        let (top, rh) = diff_rows(&prev, &cur, w, h).unwrap();
166        assert_eq!(top, 8);
167        assert_eq!(rh, 8);
168    }
169}