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:
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user