Stop the constant repaint that made the left-edge artefacts permanent

The artefacts went from occasional to continuous, and the cause was my own
mitigation for them.

lv_obj_set_local_style_prop() does NOT skip identical values -- it calls
lv_obj_refresh_style() unconditionally, which invalidates the object. A
comment in ui_set_stale() claimed the opposite and that assumption spread.
ui_update_thermostat() is called once a second by the control task and was
writing seven style properties every time, so the panel was redrawing
something forever, and glitched lines were being regenerated as fast as
anything could clear them.

Two full-screen invalidates made it worse still: one after every HA poll,
one 600ms after every touch. The premise was that LVGL never reinvalidates
pixels it does not know are wrong, so repainting would clean them up. But
the README already had the answer -- the corruption scales with how much is
written, because LVGL's PSRAM writes compete with the panel's DMA reads out
of the same PSRAM. A full repaint is 768KB of exactly the contention that
causes the problem. Both are removed.

What is left is the thing that actually helps: write less.

  - ui_update_thermostat() returns without taking the lock when nothing
    moved, and otherwise touches only the widgets whose values changed.
  - ui_set_stale() is guarded on the previous value.
  - Condition and trend icons only get a colour write when the glyph itself
    changed.

Also raises the bounce buffer from 16 to 20 scanlines. The comment saying
20 "fails to allocate once WiFi is up" was true when display_init() ran
after wifi_init_sta(); it does not any more. display_alloc() takes the
buffers from a virgin heap with ~200KB free, so the extra 12.8KB is
affordable, and each larger chunk buys the refill ISR more time to fetch the
next one before the LCD needs it. That attacks the mechanism rather than the
symptom. ~24KB internal heap remains once WiFi is up; the "Free internal
heap" log line is the thing to watch if it needs to come back down.

Both gotchas are now in the README so the next person does not reach for a
repaint again.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
mattandClaude committed 2026-08-05 21:04:23 -07:00
1 parent 3aabcac90a
commit 7a58f9838a
5 files changed
+212 -129

No files matched your search

+25 -3
View File
@@ -231,9 +231,17 @@ the buffer being displayed — copying into the live framebuffer races the LCD's
own reads, and the resulting corrupt pixels persist indefinitely because LVGL
does not know they are wrong and never reinvalidates them.
A 16-scanline bounce buffer stages each chunk in internal RAM. 20 scanlines
would be better but needs 64KB of internal RAM and fails to allocate once
WiFi is up.
A **20**-scanline bounce buffer stages each chunk in internal RAM (64KB across
two buffers). This was 16 for a long time because 20 "fails to allocate once
WiFi is up" — which was true when `display_init()` ran after
`wifi_init_sta()`. The two-phase split changed that: `display_alloc()` now
takes these buffers from a virgin heap with ~200KB free, long before WiFi has
fragmented it. Bigger chunks give the refill ISR more time to fetch the next
one before the LCD needs it, which attacks the artefact mechanism directly.
That leaves ~24KB of internal heap once everything is up. Watch the
"Free internal heap" log line — if it approaches single digits, come back
down to 18 or 16 scanlines.
### Known limitation: occasional left-edge artefacts
@@ -283,6 +291,20 @@ about PSRAM bandwidth or panel timing.
## Gotchas
- **Setting a style property always invalidates, even to an identical value.**
`lv_obj_set_local_style_prop()` calls `lv_obj_refresh_style()`
unconditionally — there is no equality short-circuit, unlike the one you
might assume by analogy with `label_set_if_changed()`. A periodic update
that restyles widgets "harmlessly" is a periodic full repaint of them.
This cost a round of debugging: `ui_update_thermostat()` ran at 1Hz writing
seven styles, which turned the left-edge artefacts from occasional into
permanent. Every update path here is now change-guarded.
- **Do not repaint to fix artefacts.** It is tempting, because LVGL never
reinvalidates pixels it does not know are wrong. But the corruption scales
with how much is written, so a full-screen invalidate is 768KB of exactly
the PSRAM contention that causes it. Two attempts at this (post-poll and a
post-interaction "repair" timer) both made it visibly worse and were
removed. Redraw *less*, not more.
- **LVGL does not bubble events to parents.** An event handler on the screen
object never sees taps that land on a child widget, which looks exactly like
dead touch zones. Light tiles therefore carry their own handlers, and the