perf(firmware): window partial refresh to the edited rows

Per keystroke, diff the new frame against the last shown one to find the
changed row band and partial-refresh only those rows instead of all 272.
E-paper update time scales with the gate rows driven, so a single edited
text line refreshes far faster. Windowed in Y only (full width, both
controllers), so the seam/mirroring logic is unchanged. Also drop the
periodic full refresh to every 64 updates — the panel stays visually
clean, so it's now mainly for longevity.
This commit is contained in:
Julien Calixte
2026-07-05 00:08:14 +02:00
parent 17fd663c85
commit 00b57bac55
2 changed files with 74 additions and 34 deletions

View File

@@ -278,9 +278,14 @@ impl<'d> Epd<'d> {
/// GxEPD2 for this dual-controller panel, the update covers the whole
/// panel (windowing isn't worthwhile — the waveform time dominates, not
/// the area).
fn update_part(&mut self) -> Result<(), EspError> {
self.set_ram_area(0, 0, WIDTH / 2, HEIGHT, 0x03, 0x80)?; // slave
self.set_ram_area(0, 0, WIDTH / 2, HEIGHT, 0x03, 0x00)?; // master
/// `y0`/`h` restrict the update to a horizontal band of rows. E-paper
/// update time scales with the number of gate lines driven, so a narrow
/// band (one text line) is far faster than the whole panel — the win that
/// makes per-keystroke typing responsive. Full width is always driven
/// (both controllers), so the seam/mirroring logic is untouched.
fn update_part(&mut self, y0: u16, h: u16) -> Result<(), EspError> {
self.set_ram_area(0, y0, WIDTH / 2, h, 0x03, 0x80)?; // slave
self.set_ram_area(0, y0, WIDTH / 2, h, 0x03, 0x00)?; // master
self.cmd(0x3C)?; // border waveform control
self.data(&[0x80])?; // VCOM
self.cmd(0x21)?; // display update control 1
@@ -301,29 +306,31 @@ impl<'d> Epd<'d> {
Ok(())
}
/// Blit a full 792×272 framebuffer into one RAM bank on both
/// controllers. Port of the full-frame case of GxEPD2 `_writeFromImage`:
/// slave gets panel bytes 0..=49 of each row in X-increment mode; the
/// master's sources are wired mirrored, so it gets bytes 49..=98 in
/// bitmap order while the address counter walks RAM 49..=0 (mode 0x02).
/// The seam byte 49 (px 392..399) lands on both; the 4 columns past each
/// controller's 396 sources aren't wired.
fn write_frame_bank(&mut self, command: u8, fb: &[u8]) -> Result<(), EspError> {
let mut buf = Vec::with_capacity(CTRL_BYTES);
for y in 0..HEIGHT as usize {
/// Blit rows `y0..y0+h` of a 792×272 framebuffer into one RAM bank on both
/// controllers. Port of GxEPD2 `_writeFromImage`, windowed in Y: slave gets
/// panel bytes 0..=49 of each row in X-increment mode; the master's sources
/// are wired mirrored, so it gets bytes 49..=98 in bitmap order while the
/// address counter walks RAM 49..=0 (mode 0x02). The seam byte 49
/// (px 392..399) lands on both; the 4 columns past each controller's 396
/// sources aren't wired. Pass `(0, HEIGHT)` for a full-frame blit.
fn write_frame_bank(&mut self, command: u8, fb: &[u8], y0: u16, h: u16) -> Result<(), EspError> {
let rows = y0 as usize..(y0 + h) as usize;
let mut buf = Vec::with_capacity(CTRL_BYTES_W * h as usize);
for y in rows.clone() {
let row = &fb[y * FB_BYTES_W..(y + 1) * FB_BYTES_W];
buf.extend_from_slice(&row[..CTRL_BYTES_W]);
}
self.set_ram_area(0, 0, WIDTH / 2, HEIGHT, 0x03, 0x80)?; // slave
self.set_ram_area(0, y0, WIDTH / 2, h, 0x03, 0x80)?; // slave
self.cmd(command | 0x80)?;
self.data(&buf)?;
buf.clear();
for y in 0..HEIGHT as usize {
for y in rows {
let row = &fb[y * FB_BYTES_W..(y + 1) * FB_BYTES_W];
buf.extend_from_slice(&row[FB_BYTES_W - CTRL_BYTES_W..]);
}
self.set_ram_area(0, 0, WIDTH / 2, HEIGHT, 0x02, 0x00)?; // master
self.set_ram_area(0, y0, WIDTH / 2, h, 0x02, 0x00)?; // master
self.cmd(command)?;
self.data(&buf)?;
Ok(())
@@ -334,23 +341,31 @@ impl<'d> Epd<'d> {
/// consistent "previous" image.
pub fn display_frame(&mut self, fb: &[u8]) -> Result<(), EspError> {
assert_eq!(fb.len(), FB_BYTES, "framebuffer must be 99 x 272 bytes");
self.write_frame_bank(0x26, fb)?; // previous
self.write_frame_bank(0x24, fb)?; // current
self.write_frame_bank(0x26, fb, 0, HEIGHT)?; // previous
self.write_frame_bank(0x24, fb, 0, HEIGHT)?; // current
self.update_full()?;
Ok(())
}
/// Show a full 792×272 framebuffer with a *partial* refresh (fast, no
/// flashing). Requires the `0x26` (previous) bank to already hold the
/// on-screen image — true after any `display_frame`, `clear_screen`, or a
/// prior `display_frame_partial`. Writes the new image to `0x24`, runs the
/// partial waveform, then syncs `0x26` to the new image so the next
/// partial update has a correct baseline.
pub fn display_frame_partial(&mut self, fb: &[u8]) -> Result<(), EspError> {
/// Partial-refresh only rows `y0..y0+h` of the panel from a full
/// framebuffer — the fast per-keystroke path (pass `(0, HEIGHT)` for the
/// whole panel). Requires the `0x26` (previous) bank to already hold the
/// on-screen image for those rows — true after any `display_frame`,
/// `clear_screen`, or a prior partial covering them. Writes the new rows to
/// `0x24`, runs the partial waveform over just that band, then syncs `0x26`
/// so the next partial has a correct baseline. `fb` is always the full
/// frame; only the given rows are used.
pub fn display_frame_partial_window(
&mut self,
fb: &[u8],
y0: u16,
h: u16,
) -> Result<(), EspError> {
assert_eq!(fb.len(), FB_BYTES, "framebuffer must be 99 x 272 bytes");
self.write_frame_bank(0x24, fb)?; // current = new
self.update_part()?; // transition previous (0x26) -> current (0x24)
self.write_frame_bank(0x26, fb)?; // previous = new, for the next partial
assert!(h > 0 && y0 + h <= HEIGHT, "row window out of range");
self.write_frame_bank(0x24, fb, y0, h)?; // current = new
self.update_part(y0, h)?; // transition previous (0x26) -> current (0x24)
self.write_frame_bank(0x26, fb, y0, h)?; // previous = new, for next time
Ok(())
}
}

View File

@@ -27,8 +27,9 @@ const CH: i32 = 20;
const COLS: usize = (epd::WIDTH / 10) as usize; // 79 characters per line
const ROWS: usize = (epd::HEIGHT / 20) as usize; // 13 lines
/// Clear accumulated partial-refresh ghosting with a full refresh this often.
const FULL_REFRESH_EVERY: u32 = 20;
/// Occasional full refresh, mainly for panel longevity — partial updates on
/// this panel stay visually clean far longer, so this is deliberately rare.
const FULL_REFRESH_EVERY: u32 = 64;
fn main() -> anyhow::Result<()> {
// Required once before any esp-idf-svc call; some runtime patches
@@ -67,7 +68,8 @@ fn main() -> anyhow::Result<()> {
// First render is full (establishes the on-screen baseline for partials).
let mut text = String::new();
epd.display_frame(render_frame(&text).bytes())?;
let mut shown = render_frame(&text);
epd.display_frame(shown.bytes())?;
let mut updates: u32 = 0;
loop {
@@ -84,24 +86,47 @@ fn main() -> anyhow::Result<()> {
}
let frame = render_frame(&text);
// Only the rows that changed since the last shown frame need updating —
// usually just the edited line, which the partial refresh windows to.
let Some((y0, y1)) = changed_rows(shown.bytes(), frame.bytes()) else {
shown = frame;
continue; // no visible change (e.g. backspace at start of buffer)
};
updates += 1;
let full = updates % FULL_REFRESH_EVERY == 0;
let t0 = Instant::now();
if full {
epd.display_frame(frame.bytes())?;
} else {
epd.display_frame_partial(frame.bytes())?;
epd.display_frame_partial_window(frame.bytes(), y0, y1 - y0 + 1)?;
}
let ms = t0.elapsed().as_millis();
log::info!(
"{} refresh #{updates}: {ms} ms ({keys} key(s), {} chars)",
"{} refresh #{updates}: {ms} ms (rows {y0}..={y1}, {keys} key(s), {} chars)",
if full { "FULL" } else { "partial" },
text.chars().count(),
);
shown = frame;
}
}
/// First and last (inclusive) framebuffer rows that differ between two frames,
/// or `None` if identical. Lets the partial refresh target just the band a
/// keystroke touched instead of all 272 rows.
fn changed_rows(a: &[u8], b: &[u8]) -> Option<(u16, u16)> {
let w = epd::FB_BYTES_W;
let mut first: Option<u16> = None;
let mut last = 0u16;
for y in 0..epd::HEIGHT as usize {
if a[y * w..(y + 1) * w] != b[y * w..(y + 1) * w] {
first.get_or_insert(y as u16);
last = y as u16;
}
}
first.map(|f| (f, last))
}
/// Apply a key event to the text buffer.
fn apply_key(text: &mut String, key: usb_kbd::Key) {
match key {