# E-ink refresh latency vs rows driven

> **Model:** on this GDEY0579T93 (SSD1683 dual-controller) panel, refresh time is
> set by **how many gate lines (rows, the Y axis) are driven** and **which
> waveform LUT runs** — *not* by the pixel column width. Three refresh modes fall
> out of that: full refresh (~1870 ms), full-area partial (~630 ms), and
> windowed-Y partial (~100–130 ms for one text line). This is the cost model
> behind per-keystroke typing, the boot splash→editor swap
> ([`../notes/boot-time-budget.md`](../notes/boot-time-budget.md)), and the
> scroll/gutter spikes.
>
> Tradeoff-curves index: [`README.md`](README.md). Docs index:
> [`../README.md`](../README.md). Driver:
> [`../../firmware/src/epd.rs`](../../firmware/src/epd.rs). Bench origin:
> [`../spikes.md`](../spikes.md) (Spikes 5 + 8).

## The model

A refresh is three serial costs:

```
set RAM window  →  clock the pixels out over SPI  →  run the update waveform
   (fixed)          (scales with bytes = rows)        (scales with gate lines
                                                        AND with which LUT)
```

Two things do **not** help, and one thing does:

- **Column width (X) is free to keep full.** The panel is a master (`0x00`) +
  slave (`0x80`) pair with the framebuffer split at the seam; every refresh
  drives *both* controllers full width so the seam/mirror math stays intact
  (`update_part` / `write_frame_bank` in
  [`epd.rs`](../../firmware/src/epd.rs)). Narrowing the *column* saves nothing —
  "the waveform time dominates, not the data clock-out."
- **Row count (Y) is the knob.** E-paper drive time scales with the number of
  gate lines transitioned, so restricting the refresh to a horizontal band of
  rows is the real win — a one-line band is far cheaper than the whole panel.
- **The LUT sets the tier.** A *partial* update (`0x22`←`0xFF`) runs a short
  waveform that only nudges pixels that changed. A *full* update (`0x22`←`0xD7`,
  the fast-full LUT) runs ~3× as many frames per pixel to fully clear-and-set —
  slower, but it erases ghosting and re-establishes a known image.

Within partial mode the latency is roughly linear in rows:

```
t_partial(rows) ≈ 90 ms + 2 ms · rows
                  └ floor ┘  └ per-gate-line slope ┘
```

Floor ≈ fixed SPI setup + border/VCOM commands + waveform ramp; slope ≈ the
per-row waveform drive. Full refresh sits in its own flat tier (~1870 ms, all 272
rows, full-clear LUT) and **cannot be windowed** — the clear waveform needs the
whole panel.

## The curve

```
  Partial-refresh latency vs rows driven (Y-band)     t ≈ 90 ms + 2 ms · rows

  ms
 1870 |=========================================  FULL refresh — separate tier:
      |                                            all 272 rows, full-clear LUT,
      |                                            un-windowable. ~3× the partial
      |                                            waveform. De-ghosts + seeds a
      |                                            known image.
      |
  630 |                                       * ← full-area partial (272 rows)
      |                                 . ·
      |                           . ·
      |                     . ·         slope ≈ 2 ms / gate line
  300 |               . ·
      |         . ·
  110 |    *  ← one text line (~10 rows): the per-keystroke path
   90 | *  ← floor (SPI setup + border/VCOM + waveform ramp)
      +----+----+----+----+----+----+----+----+----+----+----+---
      0   25   50   75  100  125  150  175  200  225  250  272  rows
```

| Mode | Rows driven | Latency | LUT | Used for |
| --- | ---: | ---: | --- | --- |
| Full refresh | 272 (all) | **~1870 ms** (measured) | full-clear | first cold-boot image; periodic de-ghost (every `FULL_REFRESH_EVERY` = 64 updates) |
| Full-area partial | 272 | **~630 ms** (measured; 680 ms at boot) | partial | deletes, caret moves, mode switches, the snackbar, and the boot splash→editor swap |
| Windowed-Y partial | ~10 (1 line) | **~100–130 ms** (estimated¹) | partial | additive per-keystroke typing |

¹ The single-line windowed figure is projected from the floor+slope model and the
Spike 5 full-area measurement; the exact bench number is still to be confirmed
from the on-device refresh log (`{mode} refresh #N … {ms} ms` in
[`main.rs`](../../firmware/src/main.rs)). The 1870 ms full and 630 ms full-area
figures are measured.

## Two things that bound it

**Ghosting caps the partial streak.** Partial updates leave faint residue, so a
full refresh every 64 updates resets clarity and panel state. You can't "always
partial" — the ~1870 ms tier is a periodic tax paid for longevity, not a mode you
can retire.

**The first cold-boot image must be a full refresh.** After power-on the `0x26`
"previous" bank holds garbage, and a partial refresh *diffs against it* — so the
very first clean paint has to be the full tier. This is why boot pays exactly one
unavoidable ~1.9 s full refresh, and why the splash (which rides it) is nearly
free while the *editor's* first frame can be a cheap partial on top. Full
derivation: [`../notes/boot-time-budget.md`](../notes/boot-time-budget.md).

## What it decides

- **Per-keystroke typing → windowed-Y partial.** Only the touched line's band is
  driven (~100–130 ms), so typing keeps up with the keyboard; the panel never
  repaints per keystroke off that line.
- **Boot splash→editor → full-area partial (~630 ms), not a second full refresh
  (~1870 ms).** The splash already seeded the baseline, so the editor rides in on
  a partial — the ~1.25 s cold-boot win recorded in the boot-time budget.
- **Deletes, caret moves, mode flips, snackbar → full-area partial.** These erase
  ink or change the panel off the cursor line, which a windowed band would ghost,
  so they take the whole-panel partial rather than a windowed one.
- **Splash + periodic → full refresh.** The unavoidable first image and the
  every-64 de-ghost.
