- ui-lvgl: navTextEntry()/TextEntry replace eight copies of popup + field +
keyboard wiring; one byte-capped insert callback instead of two.
- ui-new: drawHeadedBubble()/BubbleMark replace the duplicated DM and channel
bubble drawing in MessagesScreen.
- GpsSky::halt(): stopping the receiver ends an unfinished search but keeps
the first-fix time; keyboard cursor blink restarts after a key.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VeBrxKyHHxWsmttVkUdBrj
- GPS: stop() resets the sky state so the 'Searching' timer does not run
while the receiver is off; the status page shows it only when GPS is on.
- L2 keyboard: put the FOCUSED state back after typing a key or picking a
variant, so the cursor is drawn again after characters like a-ogonek.
- Quick panel: drop the full-screen dim (a blend per frame), so the screen
below stays interactive; tiles are one sideways-scrolling, snapping row;
the brightness slider no longer animates and only updates the backlight
when the value changes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VeBrxKyHHxWsmttVkUdBrj
The quick panel, as a phone's: pulled down from the status bar (or a tap
on it). Bluetooth, WiFi, GPS, sound, trail and live share switch at a
tap; Advert opens the advert popup (moved here from the Nodes header);
Lock locks the screen; a brightness slider. Holding a tile opens its
settings. Lock and sun glyphs added to the fonts.
Fixes from rc1 testing on the L2:
- GPS status icon: none when GPS is off (the module kept its last fix
valid, so it stayed green); grey on, green with a fix. ensureGps()
turns GPS on before trusting that stale fix.
- Screen wake: the panel wakes dark, a frame is drawn, then the
backlight fades in -- no white flash. Off fades out first.
- Power off / flat battery: the chip wakes every 30 s to look for USB
power and starts when plugged in (it slept with no wake source before,
until the reset button). Plugged in when switched off: stays off until
re-plugged.
- Message ages in a conversation refresh every 2 s, as on ui-new; the
message popup keeps its age, repeaters heard and delivery up to date.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
versionNum() read "v2.0-rc1" as plain 2.0, so a device on a release
candidate never offered the release itself (nor a later candidate). A
"-rcN" suffix now orders before the bare version: v2.0-rc1 < v2.0-rc2 <
v2.0 < v2.0.1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/prefs holds a header (magic, format version, size, CRC-32), the node's
position and NodePrefs as it is in memory, in place of /new_prefs' two
hand-kept field lists and 0xC0DE00xx schema bumps. A new field goes at the
end of the struct; moving one bumps PREFS_VERSION. sanitize() keeps every
value in range.
The first boot after an update from before 2.0 imports only the first
block of the old file, laid out the same in upstream MeshCore and Solo
(name, position, radio, BLE PIN, scope), then deletes it; everything else
starts from the defaults. Contacts, channels, scopes, waypoints and trails
are untouched.
Gone: the page-order and GPS-interval migrations, the unused
trail_interval_idx / trail_units_idx / repeat_extra_scopes; fav_sort_off
and msg_wake_screen_off become fav_sort and msg_wake (on by default).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node detail: a card with a compass rose and the hop chain over the info;
the bar keeps Message / Admin / Add, Ping and Navigate, the rest goes to a
"More" group under the info (favourite toggled in place, delete by a second
tap). Nodes: an arrow by each distance. A trail's height profile in Map
tools and a saved trail's popup. Diagnostics > History: battery, noise,
SNR and traffic charts. Clock > Calendar: today, the weekend and alarm days.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OLED gets the arrows towards nodes in Nodes and the Path row with its
hop rings in a stored node's detail (its position on one line to make the
room); no compass rose. Trail gets a height-profile view on both. The
Status history, trail profile and calendar come from ui-core. A sim hook
turns on the advert's position for tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The history of the noise floor, battery, traffic and SNR, a trail's height
profile (with a sampler for trails read point by point off the card) and a
month's calendar, moved out of ui-new so ui-lvgl can draw the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The landscape build's 8x13 digits stand 9 px, so its status, page and
check glyphs and the row-heading map markers come in a 7 px set instead
of 5. The battery outline, in the header and on the Status tile, leaves
its corner pixels out.
The page icons go back to a row of their own under the header: beside
them on the header line only three status icons fitted, and the rest
went missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Message ages, the GPS search time and the sky plot changed every second,
and each change is a panel refresh. On e-ink an age under a minute reads
"<1m", the search time counts minutes, and the sky redraws every 15 s
(at once after a key).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tall screens stack the Sky view and the map over their rows, show more
recent chats, give favourite cards the heard age and distance, and put
the trail map under the trail summary. Wide screens put the page icons
on the header line.
E-ink only (Features::IS_EINK):
- a month calendar under the clock and on the portrait lock screen, with
the alarm's days marked; the lock screen also lists unread messages,
the alarm and the clock fields
- 8-hour charts in Status: battery, noise and SNR, mesh traffic
- an elevation profile under the trail map, with the climb and the top
- a compass rose and the path's hops on a contact's detail, arrows by
the distance in Nodes rows and on favourite cards
- an ordered-dither fade under chart lines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A screen with room for nine lines or more shows each message bubble
fully wrapped instead of one line. The bubble's height is measured at
the width it is drawn at.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wrapLines carried the overflowing space's word to the next line too
early, so bubbles wrapped short of their width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each point keeps the GPS altitude in what was padding, so a point stays
16 bytes. The save format goes to version 2; version 1 files still load,
with the altitude unknown. GPX export writes <ele>.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The L1 e-ink solo build splits in two, one per screen shape, so each
screen can be laid out for a single shape. Settings > Rotation now only
turns the screen upside down, and the joystick turns with it.
Landscape writes in misc-fixed 8x13 at the panel's own resolution
(EINK_LARGE_FONT); portrait keeps the 6x9. All drawing moves into
EinkGfxDisplay, which the panel driver and the simulator share; the
FreeSans fonts and the CRC32 library go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Big / Compact lock look is ui-new's; the shared schema row did nothing
on the L2's slide-to-unlock card.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ui-core: Diagnostics::systemLines, NearbyModel::typeShort,
rptctl::currentPresetName, RoomSessions::pendingKey, settings::isSwitch,
UiCore::reconcileDMUnread.
- ui-new: the retired Recent page, fmtAlt, the battMvToPercent wrapper,
setFavouriteChannelSlot, AccordionList onHeader/visibleCount, drawBoxedIcon
and seven unused icons; info::row folded into info::valueRow.
- ui-lvgl: onOpenNav, TileDownloader::withTrails; the trail header size is
one constant (TRAIL_HDR) for live and saved trails.
- Home quick panels save prefs only when a value changed.
- Comments brought in line with the code (status bar reserve, settings rows,
page order, SH1106 frame skip, UiCore header).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The redraw, alert, splash and sensor timers compare millis() wrap-safe
(refreshDue(), alertShowing(); 0 = now / none), so the screen keeps
refreshing past the 49.7-day rollover.
- A failed send leaves the keyboard with its text ("Not sent" in the field
label, as alerts don't show over it) or the quick-message list with
"Send failed", instead of dropping to Home. No "Sent" echo.
- The dashboard's Sats field shows "--" with the GPS off, not a stale count.
- Brightness and volume in Settings stop at both ends instead of wrapping.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Home clock page draws HH:MM in the boot splash's slanted digits, with a
seconds bar, a centred date and the clock fields as columns. The lock screen
keeps the status bar, puts the big clock under it and shows unread messages
in a pill; the unlock hint appears for a few seconds after a key press on the
lit screen (not the waking one). Settings > Display > Lock clock adds a
compact look: a small clock with the date beside it and the clock fields as
rows (NodePrefs lock_compact, schema 0xC0DE0033). 12-hour time sets AM / PM
beside the digits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Battery curve: NodePrefs batt_curve_mv[11] (voltage at 0..100 %, all
zeros = built-in LiPo curve, sentinel 0xC0DE0032). ui-core/Battery.h
uses it for every percentage. L1: Settings > System > Batt curve list
(Left/Right 10 mV, Reset); L2: Settings > Power > Battery curve chart
with draggable points and a line at the cell's voltage.
- L2: the Nodes map is the full navigation map plus the nodes, Back
returns to Nodes; the favourite button is lit while it is one; pinning
to the Home dial uses a thumbtack (fonts regenerated); minimap dots
follow the accent; arranging tiles shows where they land while dragging;
emoji row in Diagnostics > Font.
- Sim hooks: sim_open nodesmap / accent@N / homeedit / battcurve.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The base class matched an ACK carried in a PATH packet without going
through onAckRecv(), so the device UI's delivery marker never saw it and
kept resending the DM until a plain ACK arrived. MyMesh now passes it on
to the listener as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Icon pages become quick panels: two or three rows (Settings, Messages,
Bluetooth, Advert, Tools, Power) inset from the edges, the last one
opening the full screen. Brightness and auto-advert are edited in place
(Enter, Left/Right, Enter/Back saves once).
- Tools panel shows the two tools used last with their live state.
- Map page: preview on the left, GPS / trail / live / target on the right.
- Favourites as soft cards; Status tile with a battery spark line.
- Status bar without boxes or blinking; BT only when connected, GPS
search glyph without a fix.
- Redrawn mini icons (advert waves, cog, wrench, messages); big hovering
icons removed. Tools list is text only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
drawScreenHeader replaces drawCenteredHeader on every ui-new screen: the
title in normal case, page dots instead of "TRAIL 1/3", the menu hint, no
icon (the screen was just picked by one). Accordions in Settings and Tools
show a disclosure triangle instead of +/-, Tools sections drop their icons.
Form "[Save]" rows and "+ Send" share drawButton; form fields use valueRow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InfoKit gains valueRow (value at the right edge, cut and scrolling when
selected), switchRow and listRow (exact ON/OFF becomes a switch). Settings,
Live Share, Locator, Repeater, Bot, Auto-Advert, alarm, GPIO and popup value
rows use them; units after a space, "Off" in title case, Trail summary as
blocks with a big distance and a points meter, "+ Send" as a soft pill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home's Radio, GPS and Sensors pages become one Status page: four tiles split
by dotted rules (radio, GPS, battery, nodes), each a headline over a detail.
Enter opens the Status screen, tabs Radio / GPS / Sky / Power / Mesh /
System, which also takes over Tools > Diagnostics and Tools > Satellites
(the sky plot now lives in SkyView.h). GPS on/off moves to Enter on its tab.
New InfoKit.h holds the shared pieces, sized from the font: label/value row
(a long value takes its own line), centred section title, meter, chip, big
number with unit, switch, history line (one sample a minute) and a block
flow that scrolls a tab by whole blocks.
The Status page keeps the old Radio bit and shows when any of the three old
pages did; the save format is unchanged. The L1 degree sign was a cp437
byte the UTF-8 renderer drew as a box; now U+00B0. The perf walk covers
every Home page and Status tab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DisplayDriver gains slideBegin()/slideCompose(): the driver keeps the frame
on screen and, in the next one, moves it out while the new rows follow.
SH1106 does it on its 1 KB buffer (+1 KB RAM for the copy), the sim on its
canvas; drivers that can't read their frame back keep cutting over (e-ink).
Home slides the content below the page-icon row in 220 ms with an ease-out
(the whole screen to and from the header-less clock), and the current-page
pill glides between icons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Splash: the L2 wordmark and lettering, shared through ui-core/Lettering.h,
rise in with loading dots under the version line (static frame on e-ink).
- Home: the header shows the time once the clock is set; icon pages carry
only their title. One 32 px icon family (3 px strokes) bobs 2 px over a
dithered blob shadow that shrinks as it rises; the unread badge rides on
the Messages bubble; Bluetooth off is the icon at half density.
- Soft corners on every selection, the page pill, tabs and popups;
dotted scrollbar track with a rounded thumb; the tab bar lays out whole
tabs and shows edge arrows only in free space.
- DM bubbles drop the sender row and wrap to a second line, with the
delivery mark and age closing it.
- Keyboards name what is typed (channel, contact, Password, Scope...)
and show characters left near the limit; PIN entry has its own preview.
- Loading dots for waits (login, scan, GPS) and signal bars in place of
raw RSSI in Nearby.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DisplayDriver gets drawSoftRect / fillSoftRect / drawPanel (the unread badge's
corner knockout; a panel also clears a 1 px moat so lines below stop short of
its border). Used by alerts, popups, the accent picker, the lock hint, chat
bubbles and the send button.
Fixes: no "Msg:" toast for a message landing in the open thread; Nodes scan
text fits the screen; the PIN pad skips the blanks beside 0; Auto-Advert is a
plain value row; the Admin picker says what it needs when empty; the GPS page
shows "-" instead of 999 before a first fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opens each screen directly, types keys, and prints per step over USB: frames
drawn/sent, render and flush time (CPU cycle counter), heap used/free/largest
block, every task's stack headroom, and a screen dump. Measurement only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New message, Nodes (and its Scan popup), Admin and Messages show their
items through one virtual list: a screenful of rows, handed to the next
item as one scrolls out, the scroll extent from the items' heights. 256
contacts: 522 objects and ~950 ms of filling in (frames up to 96 ms)
before, 19 objects at once now; a scrolled frame 28.5 -> 11.7 ms. Nodes
257 -> 49 objects. A new message rewrites Messages' rows in place
(46 -> 20 ms) instead of building the screen again; it's rebuilt only
when "Read all" comes or goes. A held row keeps what it shows until let
go. Perf tools: -D PERF_LISTS, -D PERF_LIST_MUL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows still to build wait while a screen slides in, so its frames aren't
a fill budget apart (Nodes: 12 frames of 24 ms instead of 7 of 33, up to
65). The crash dump in flash is read at boot, not on the first
Diagnostics (build 54 -> 20 ms). The perf tools time Nodes too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel takes each 24-line band by DMA while LVGL draws the next one
into the other buffer (two 15 KB buffers in internal RAM; LovyanGFX's
4092-byte DMA descriptors leave PSRAM unaligned for the S3's DMA, which
showed as shifted and smeared bands). LVGL draws in the panel's byte
order, and so do map tiles and the frozen screen under a popup, so
nothing is swapped on the way out. Full frames ~10-25% faster (page1
19.9 -> 14.7 ms, Home 21.4 -> 18.2 ms). Without the internal RAM the
screen is sent as before. The arduino2 env keeps the old way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recording / live share and the download stack in a column at the top
centre, empty when nothing runs; the recording pill is plain text with
only its mark coloured (red stop, accent pause / live) -- all-red text
was hard to read over the map. The credit, scale bar and zoom share one
pill bottom left (a tap shows the credit). Every map pill is 22 px with
round ends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A popup over a still screen is frozen and dimmed as it opens, so the
panel's rise redraws only the panel: 6-8 frames of 19-25 ms instead of
3-4 of 45-65 ms. Picker options are one label each (time zone list
97 -> 52 ms to build). The bottom popup no longer animates a dim of 0.
Repeater updates its hint in place (130 ms rebuild gone); Auto power,
Imperial units and Relay on rebuild the screen after the knob has
moved (rebuildSoon). -D PERF_CONTROLS times every control on the
settings screens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 404/503 answer (OpenTopoMap renders on demand) went to an immediate
2 s retry and counted as network loss, so each tile then waited up to
60 s. Answered errors now go straight to the retry list; long pauses
only when there is no answer at all. Three retry rounds (20/60/120 s)
for the tiles still missing, transfers cut after 30 s, failures logged
on serial.
Download popup tidied: title + floating close, percentage instead of
tile counts (also on the map pill), no dead zoom row, no finish toast
after Stop or while the popup shows the result.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A group row's text column (and an info row's key) grows into what the
controls on its right leave, and LVGL lays out children before their
parent, so its labels were first measured 1 px wide; LVGL also works out a
new label's height at its old width (0) before the real one. Each height
that changed then took another pass up the screen: 4-6 passes per screen.
rowsFit(), run before a frame's layout (LV_EVENT_REFR_START) and by
layoutNow() in place of every explicit lv_obj_update_layout(), gives each
new row's growing child the width the flex will give it, worked out from
the other children's styles and text, and labelFit() measures wrapping
labels at their real width up front. The grow stays, so a control that
can't be sized ahead only falls back to the old passes.
Layout on the device: Display page 34 -> 24 ms, Bot 33 -> 18, Quick
37 -> 22, Radio 26 -> 18, Diagnostics 20 -> 12. Sim screenshots of 26
screens unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An infinite lv_roller measures its whole option text (repeated three
times) on every style change, layout pass and frame. The hand-drawn wheel
draws only the visible rows, with the same look, drag, throw, tap and
wrap-around. Clock first frame 99 -> 54 ms (layout 42 -> 12 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UiCore::setGpsEnabled() wrote the prefs to flash at once, which stalled
the loop mid-animation. It takes save = false now (default true, the L1
unchanged); the L2 marks the prefs changed and writes them on leaving
the screen, as its other switches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vector map isn't finished: without the flag it stays off whatever
was saved, and its switch and the Vector regions row are gone from the
map tools. The code stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by counting flushes on idle screens in the sim:
- The compose field's cursor blinked (a redraw every 400 ms) as soon as
a conversation opened, the keyboard hidden: lv_keyboard_set_textarea()
focuses the field, and LVGL only stops a blink when the cursor moves.
fieldFocus() sets the state and nudges the blink; also used for the
WiFi and channel fields, which left theirs running.
- lv_obj_align_to() restyles on every call, so the status bar's icons
redrew every second; placeLeftOf() moves them only when they move.
- Text colours set every second (nav bar, GPS status, Diag errors) go
through setTextColor(), which skips an unchanged colour.
- A LV_LABEL_LONG_DOT label writes its "..." into its own text, so
setText() never matched and rewrote it every refresh (the nav bar's
hint); labelShows() reads past the dots.
- The map area, the area-outline, trail and vector-label layers were
invalidated on every map refresh (every 3 s, a full-map redraw); now
only when the view or what they draw changed.
Idle map: 17 -> 1 flush in 5 s; list and settings screens: 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor has its own 80 % dim layer, not a popupOpen() one, so every
key press redrew the list under it and blended the dim over it. It now
freezes the screen like the popups (freeze::now); the frozen image takes
the dim layer's own level instead of assuming 60 %.
Keyboard redraw 38.7 -> 29.7 ms, full frame 46.3 -> 39.7 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The map screen's own fill is only painted where the map area doesn't
cover it (the strip it uncovers as it drifts in); the area and the
opaque status bar hide the rest. ~2 ms a frame.
- With every visible cell holding a tile, the map area drops its fill and
grid, which lie under the tiles anyway, and still reports that it
covers what's below (coversOwnArea, shared with the frozen popup
image). The Home minimap drops them the same way. ~5 ms a frame.
Full map frame ~50 -> 46-48 ms on the L2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kept for the optimisation pass before a release:
- perfDrawProfile(): a full redraw timed per object (its own drawing),
totals by kind and the costliest listed -- on settings pages, Contacts,
Home, the map and under a frozen popup; the screen's entrance frames too.
- Per screen: build, layout, first draw and redraw apart.
- A popup over Home: a full frame dimmed live and frozen.
- The reset reason and a core dump summary at start.
- -D PERF_WRAP_TEXT with -Wl,--wrap= of lv_text_get_size_attributes,
lv_layout_apply, lv_obj_get_style_prop_internal and lv_obj_send_event:
text measured, flex runs, style reads and events per screen.
- -D PERF_KEEP_USB leaves the USB popup up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once a popup's dim is up, the screen below is drawn once into an image
(LV_USE_SNAPSHOT), darkened as the dim layer darkens it, and shown opaque
over the screen with the dim layer cleared. LVGL then starts drawing from
that image; an lv_image with no background never reports that it covers,
so it answers the cover check itself. A full frame under a popup: 45.7 ->
32.6 ms; freezing costs ~29 ms once, as the popup settles.
One popup at a time (a second one dims live), a strip keeps the status
bar dimmed under top-layer popups, the image goes with its popup, and the
popup dims live again if its screen goes first. POP_BOTTOM (over a live
map) isn't frozen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LVGL lays out children before their parent, and a new box's size is only
known after a pass, so every level sized in % of the one above cost a whole
extra layout pass over the screen. innerW() works the width out from the
styles instead; fillWidth() sets it on infoCard, infoLine (every row),
noteLabel and scrollList, and the screen body is px-wide.
Display & power page: 101 -> 76 flex runs, layout 68 -> 54 ms, first
frame 161 -> 146 ms; the other settings pages and Radio / Bot / Quick
alike. Four passes remain: a row's text column grows beside its control,
so its width is only known after the row's own layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured on the L2 (steady full frames): Settings page 37.7 -> 31.4 ms,
Display 41.5 -> 37.9, Contacts 34.6 -> 32.9, Home 37.3 -> 32.4.
- styleSurface() leaves boxes in the page colour unpainted; styleOpaque()
for the screen and the status bar, which things slide under.
- group() cards clip their corners only while a row is held
(rowPressable(), shared by the tappable rows).
- rowSlider(): the track an opaque tint (theme::mix(), also behind
ACCENT_DIM) instead of 20 % accent blended every frame.
- Map buttons, pills, scale, nav bar and the Home minimap labels opaque.
- Screens drift in without the fading cover: its full-screen blend was in
every entrance frame (page 39 -> 29 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PNGdec (bitbank2, 1.1.6) decodes a tile a row at a time straight into
RGB565, without lodepng's 256 KB RGBA buffer; the trail overlay goes through
it too, blended by hand for palette-with-alpha and RGBA. It lives in its own
file (ui-lvgl/TilePng.cpp) because its zlib headers put DONE/EXTRA in the
global namespace. lodepng stays the fallback and the sim's decoder.
Same 20 tiles: decode 78 -> 50 ms, trails 42 -> 17.5 ms, a whole tile
170 -> 105 ms; the map with trails is pixel-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>