Skip to content

Repaint only what changed between display updates - #26

Open
bettio wants to merge 15 commits into
atomvm:mainfrom
bettio:damage-tracking
Open

bettio wants to merge 15 commits into
atomvm:mainfrom
bettio:damage-tracking

Conversation

@bettio

@bettio bettio commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator

Every update rendered and transferred the whole screen, even when a
single label changed. The drivers now keep the last display list,
compare the new one with it and repaint only the areas that differ, so
an identical list costs nothing. The retained list is released after
10 seconds without messages.

Pixels no item covers are now black on the DCS LCDs and SDL and off on
OLEDs. Memory LCD and OLED paths are compile-tested only.

See also: #20, #23

bettio added 15 commits October 12, 2026 15:46
Partial screen updates need a compact description of the areas that
must be repainted. DisplayDamage collects up to eight rectangles
clipped to the screen, drops rectangles already covered and, once full,
merges new ones into the rectangle that grows the least, so the set is
always a superset of what was added while staying bounded in size.
rows_span() serves drivers that transfer whole lines or 8-row pages.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Partial updates need the areas where the new display list differs
from the one on screen. Items are matched as a common subsequence,
comparing fields but never image pixels (a different buffer counts as
a change, to keep updates quick); shapes compare by geometry and
scaled images include their flips. The bounding box of every unmatched
item is damaged, so pixels outside the damage keep their color.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Partial updates need the binaries of the previous display list to stay
valid until the next list has been compared against it. Let the driver's
process_message_fn return whether it kept the message, and have the task
dispose it only when it did not. Every driver still returns false, so
nothing changes yet.

Also leave a note in the e-paper driver about skipping refreshes of
identical display lists once it adopts the shared frame state.

Signed-off-by: Davide Bettio <davide@uninstall.it>
The scanline renderer implicitly stopped at the screen width. Partial
updates need to stop at the right edge of the area being repainted, so
make the limit an explicit x_end argument. Every caller passes the
screen width, no behavior change.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Partial updates need to know what is on screen: the items of the last
update, the message owning the binaries they point to, and the areas
the next update must repaint regardless of the diff. Keeping the
message alive until the next commit is what makes comparing image data
by pointer sound: libAtomVM shares reference counted binaries across
message copies, so the same binary yields the same pointer frame after
frame and nothing the old items point to is freed meanwhile. No driver
uses it yet.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Use the shared display_frame and display_diff modules: only the areas
that differ from the previous frame are rendered and copied to the SDL
surface, and identical lists cost nothing. Pixels no item covers are
black, so the output is a pure function of the display list.

AVM_SDL_DISPLAY_VERIFY_DAMAGE=1 renders every frame again from scratch
and reports pixels differing from the incrementally updated frame
buffer; test_damage.erl exercises it, headless with
SDL_VIDEODRIVER=dummy.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Use the shared frame state to repaint just the rectangles that changed,
each through its own CASET/PASET window: a changing label costs a few
scanlines instead of a whole frame and an identical list costs no SPI
traffic. The window is set before acquiring the bus and the last DMA
transfer awaited before releasing it, since the bus lock is not
reentrant and the DC line must not toggle during a transfer.

Pixels no item covers are cleared to black, so the panel content
depends only on the display list. The area painted by draw_buffer is
invalidated so the next update repaints it. The profiling line gains
the rectangle count and the pixels sent.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Use the shared frame state and skip the 8-row pages the damage does not
touch. Touched pages are still sent in full, so the I2C addressing does
not change.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Sharp memory LCDs address lines individually, so use the shared frame
state to render and send just the lines the damage touches. When
nothing changed, send the two-byte VCOM-only frame so VCOM keeps
alternating on every update as before.

Both DMA buffers are always allocated, so the dead single buffer path
is removed.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Describe what each driver repaints, that uncovered pixels are black on
the DCS LCDs and SDL, how draw_buffer interacts with updates, and how
to run the host unit tests and the headless SDL verification.

Signed-off-by: Davide Bettio <davide@uninstall.it>
mono_draw_x leaves pixels no item covers untouched, and the bitmap it
draws into was cleared only once per page: a lit pixel smeared down to
the end of its page wherever nothing covered the rows below it.
Uncovered pixels are now off, as documented.

Signed-off-by: Davide Bettio <davide@uninstall.it>
The items, the ufont bitmaps, the message heap and a reference on
every binary in the list stay allocated between updates, which only
pays off while updates keep coming. After 10 seconds without messages
the display task releases them and the next update repaints the whole
screen, as the first one does. Applications refreshing less often pay
one full repaint per update, as they did before partial updates;
interactive ones never idle that long.

Signed-off-by: Davide Bettio <davide@uninstall.it>
The ESP32 display task unregisters a font on deregister_font; the SDL
plugin reported the command as unexpected, so a font could not be
replaced under the same handle on the host.
Text drawn with a ufont is rasterized into a new buffer on every
update, so comparing buffer pointers found it changed every time and
it was repainted on every update. The item now keeps the text, the
font handle and the color its pixels are a function of, and two
rasterizations of the same source compare equal. Registering or
unregistering a font bumps a generation the item records too, so text
rasterized before a font was replaced under the same handle is
repainted.
A retained display list is compared with the next one, never drawn, so
the bitmaps of its ufont texts were kept for nothing: at four bytes per
pixel they outweighed everything else the frame holds. Parsing now only
measures the text; each driver rasterizes the texts inside the area it
is about to draw before sending the first row, so an unchanged label
outside the damage costs a measurement instead of a zlib inflate per
glyph and a bitmap, and the pixels are released at commit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant