Context
From the 2026-07 display/FLPR architecture review (C1).
DisplayDriver::present() takes no bulge-exclude region, so the two backends diverge around the
seam instead of behind it:
- The FLPR map plane bypasses the seam:
MapDisplay::render_present (main.rs) matches on
overlay_span and calls Ls021Flpr::present_within(exclude) on the concrete type — the one
place the "board-agnostic" map plane touches a specific backend's inherent API.
- The ST7789 backend can't clip at all:
display/st7789.rs documents an accepted
"dev-only-best-effort" flash — a dirty.map redraw landing during a live hold pushes clean
rows over the bulge, blanking it until the overlay task's next ~8 ms tick.
Both problems are the same missing parameter.
Implementation plan
- Trait change in
obc-fw-nrf54l/src/display/mod.rs:
fn present(&mut self, exclude: Option<(u16, u16)>) -> bool — exclude = Some((y0, rows))
means "the rows [y0, y0+rows) belong to the overlay plane this frame: update the diff store
for them (it tracks the clean fb) but do not push them". Document that contract on the trait.
- Hoist the shared skeleton into obc-platform: the "diff the whole frame, clip each changed
span around the exclude interval, emit the clipped spans (+ whole-frame-minus-exclude fallback
on span overflow)" logic currently lives only in Ls021Flpr::present_within. Move it to
obc-platform (e.g. RowDiff::diff_clipped(fb, stride, exclude, push_span) composing the
existing diff + clip_span), with host tests. Both backends then share it verbatim.
- FLPR backend (
display/ls021_flpr.rs + ls021_flpr.rs): present(exclude) becomes the
old present_within; delete the public present_within and the match overlay_span in
MapDisplay::render_present — the map plane calls the seam method unconditionally.
- ST7789 backend (
display/st7789.rs): present(exclude) runs the same diff_clipped,
banding only the clipped spans — the mid-hold flash is gone. For the exclude span to be real
on this backend, give the tft MapDisplay the input_plane handle (the FLPR variant
already holds it) so poll_overlay returns the live span instead of hardcoded
(false, None). Its hold_progress() can then also return the real value instead of 0.0
(fixes the in-screen Reset-bar fill never moving on tft).
- Convergence: after 3–4 the two
MapDisplay::render_present bodies are identical — shrink
the cfg surface where it falls out naturally (a full MapDisplay merge is optional stretch,
not required here).
Semantics note for ST7789: excluded rows keep their on-glass content until the overlay plane's
next bulge repaint / trailing clear (≤ 8 ms away) — exactly the FLPR discipline, and strictly
better than today's flash-then-repaint.
Verification
Part of epic #353.
Context
From the 2026-07 display/FLPR architecture review (C1).
DisplayDriver::present()takes no bulge-exclude region, so the two backends diverge around theseam instead of behind it:
MapDisplay::render_present(main.rs) matches onoverlay_spanand callsLs021Flpr::present_within(exclude)on the concrete type — the oneplace the "board-agnostic" map plane touches a specific backend's inherent API.
display/st7789.rsdocuments an accepted"dev-only-best-effort" flash — a
dirty.mapredraw landing during a live hold pushes cleanrows over the bulge, blanking it until the overlay task's next ~8 ms tick.
Both problems are the same missing parameter.
Implementation plan
obc-fw-nrf54l/src/display/mod.rs:fn present(&mut self, exclude: Option<(u16, u16)>) -> bool—exclude = Some((y0, rows))means "the rows
[y0, y0+rows)belong to the overlay plane this frame: update the diff storefor them (it tracks the clean fb) but do not push them". Document that contract on the trait.
span around the exclude interval, emit the clipped spans (+ whole-frame-minus-exclude fallback
on span overflow)" logic currently lives only in
Ls021Flpr::present_within. Move it toobc-platform(e.g.RowDiff::diff_clipped(fb, stride, exclude, push_span)composing theexisting
diff+clip_span), with host tests. Both backends then share it verbatim.display/ls021_flpr.rs+ls021_flpr.rs):present(exclude)becomes theold
present_within; delete the publicpresent_withinand thematch overlay_spaninMapDisplay::render_present— the map plane calls the seam method unconditionally.display/st7789.rs):present(exclude)runs the samediff_clipped,banding only the clipped spans — the mid-hold flash is gone. For the exclude span to be real
on this backend, give the
tftMapDisplaytheinput_planehandle (the FLPR variantalready holds it) so
poll_overlayreturns the live span instead of hardcoded(false, None). Itshold_progress()can then also return the real value instead of0.0(fixes the in-screen Reset-bar fill never moving on
tft).MapDisplay::render_presentbodies are identical — shrinkthe
cfgsurface where it falls out naturally (a fullMapDisplaymerge is optional stretch,not required here).
Semantics note for ST7789: excluded rows keep their on-glass content until the overlay plane's
next bulge repaint / trailing clear (≤ 8 ms away) — exactly the FLPR discipline, and strictly
better than today's flash-then-repaint.
Verification
diff_clipped: spans clipped around the exclude interval; the store isupdated for excluded rows (a later present with no exclude does not re-push unchanged
excluded rows); span-overflow fallback still respects the exclude.
clip_span/rowdiffhost tests pass unchanged.grepshows no caller ofpresent_withinoutside the backend, and nobackend-concrete display call in the map plane.
clear intact (parity with today).
tft: hold a button while the map is redrawing (e.g. zoom during a hold) — thebulge no longer blanks; the Reset screen's hold-to-confirm bar now fills.
Part of epic #353.