Repository navigation
feat(lua): support Bitmap sources in LVGL image objects - #7947
thomaskistler wants to merge 15 commits into
Conversation
Let widgets manage decoded image lifetimes through direct Bitmap sources while preserving filename support. Retain Bitmap userdata while displayed, support explicit clearing, and account for alpha conversion within the existing bitmap memory budget. Document the API and cover source replacement, clearing, conversion, and lifecycle cleanup with focused tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Thanks for this, Bitmap sources for One behaviour change for existing scripts is worth calling out in the PR description: 1.
|
Address review point 1. Release native Bitmap resources immediately but keep the parent-owned registry slot and handle until deferred cleanup. Delete image wrappers exactly once, guard deleted windows, and retain the main Lua thread. Reject only explicit parents whose image window was deleted, so windowless objects such as labels still fall back to the script window. Include lifecycle, ref-slot reuse, coroutine and windowless-parent regressions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Address review point 2. Parse the file/bitmap source after all other image parameters, so a rejected property never replaces it. Validate the new source and acquire its ref and conversion buffer before releasing the old source, so validation and allocation errors leave the current image intact. Read source fields with raw lookups so params table __index metamethods cannot run during the update. Include rejected-update, metamethod and decoded JPEG zero-copy regressions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Address review point 3. Protect image construction and explicitly clean failed objects, native windows, registry refs and conversion charges. Restore parent context on failure, including child construction. Include repeated constructor failure and resource-baseline regressions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Address review point 4. Remove the experimental capability constant and document main's Bitmap addition in 3.0.0 without guessing a 2.12 backport release. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Address review point 5. Allow file plus bitmap=false to select the file source regardless of table traversal order, while rejecting file plus an actual Bitmap. Document and test both source-field orders. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lvgl.clear(image) cleans the StaticImage window, which deletes its internal LVGL image object without updating StaticImage::image. A later source change or image teardown then deleted the stale pointer again. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lvgl.build() and children of page/dialog constructors set a temporary parent while building nested objects but never restored it when a child raised. Later constructors were attached to that parent, which might already have been released and deleted. Build nested objects in a protected call and restore the previous parent before rethrowing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
@pfeerick thanks for the thorough review! I've pushed seven follow-up commits to this PR. Commits 1–5 address your five points. Commits 6 and 7 fix two further issues found while re-reviewing the lifetime and parsing paths. Each commit includes regression tests. The PR description now calls out the 1. Native window deletion freed the image's Lua handle early (047256d, review point 1)Issue: The image's Fix: The delete handler now only releases the Bitmap (and any conversion buffer) and forgets the window. The handle and its registry slot stay valid until the parent's normal cleanup, which then deletes the wrapper exactly once. Image methods, callbacks and visibility checks are guarded while the window is gone. The new deleted-parent check for 2. A failed
|
|
Might be a good idea to ask your AI assistant to do a code-review on the PR (i.e. on it's own work), as Claude is still griping about a few things... I can continue to give you the feedback if you want me to but it would be quicker for you to get it to self-assess and loop through fixing any issues it finds. Code review feedback
These are the problems PR #7947 introduces or makes worse. The others either already existed on main, are improved by the PR, or aren't bugs.
The agent also found two minor issues it attributes to the PR:
|
Ok. Ironically, I did review it with Claude Opus 5.5 at max effort. That's how it found issues 6 and 7 above. But I will run it one more time. |
An image callback (visible, color, size, pos, opacity or file) that calls lvgl.clear() released and deleted the image while its callRefs() and pcallUpdate*() frames were still running, so they used a freed object. Free released image wrappers from MainWindow::emptyTrash(), the same top-level point that frees closed windows, and detach them from their window on release so the remaining callback code is a no-op. Also skip setSource() when a file callback deleted the image's window. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lvgl.clear(image) cleaned the StaticImage window, deleting its internal LVGL image object along with the image's Lua children. The image then stayed blank, because neither a Bitmap source nor an unchanged filename or callback result is applied again, while a Bitmap source stayed referenced and charged to the budget. The internal image object is not one of the image's children, so keep it when clearing; only content added on top is removed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Switching an image from a filename string to a filename callback marked the source changed with an empty path, so refresh() removed the image and it stayed blank until the next callRefs() evaluated the callback. Leave the displayed file in place until the callback's first result is applied. Switching from a Bitmap still releases its pixels immediately. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The new conversion buffer is allocated before the old one is released, so a replace keeps the current image if it fails. Document that this briefly needs budget for both conversions, and that lvgl.clear(image) removes only the image's children. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
GC steps while image:set() parses its parameters can run a __gc finalizer that clears the UI or closes the image's window. Parsing then continued, taking a new Bitmap ref, conversion buffer and budget charge, and callback refs, on an already released image. Nothing freed them, and the conversion charge outlived the Lua state. Parse image parameters in a protected call. Afterwards, release everything if the image was released meanwhile, or drop the new Bitmap if its window was deleted, then rethrow any parse error. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A filename callback that replaced the image source while running, for
example with image:set({bitmap=b}) or {bitmap=false}, had its returned
path applied afterwards. That overwrote the new Bitmap or cleared state,
kept the Bitmap charged while a file was shown, and dropped the new
source for good since the callback was gone.
Count committed source changes and apply a callback result only when no
change happened during the call.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
callRefs() returned early for an image whose window was deleted natively, so a pending lvgl.clear(image) never released its children. Their handles, callbacks and anything the callbacks captured stayed referenced until the whole script UI was torn down. Still run no callbacks for such an image, but release the children of a pending clear. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
@pfeerick I pushed seven more commits that address the five points:
Another review pass found two related edge cases, which are fixed too:
All 286 native gtests pass, and an independent repro-based review of the latest head found no further issues. I haven't tested this revision on hardware yet. |
|
Not keen on overloading the exiting image class this way to handle bitmaps (which could eventually be deprecated and removed). |
@philmoz That’s fair. The goal is to let Lua widgets control image caching to avoid repeated JPEG decoding. In AeroGrid’s map panel, the shared cache can evict tiles that are still visible, making panning very slow. I used I’m not attached to that API or extending the existing image class. An LVGL-native alternative would work too. What approach would you recommend for keeping widgets' active images decoded and releasing them when they’re no longer needed? |
|
lvgl.image is a wrapper for the StaticImage class. |
Fixes: N/A (feature; no linked issue).
Summary of changes
This extends the Lua LVGL image API to accept existing
Bitmapobjects. Widgets can then load, retain and release decoded images themselves, rather than relying entirely on LVGL's filename cache.The API remains
lvgl.image([parent, ]params), with a newbitmapsource inparams:The existing
filesource remains supported; specify eitherfileorbitmap, not both.Motivation
While I was adding a map panel to the AeroGrid widget, I ran into performance problems with map tiles related to how LVGL manages its image cache. A map view made up of multiple tiles can easily exceed the default eight-entry cache when panning and zooming. This can cause the same JPEGs to be decoded repeatedly, even after all the tiles needed for the current view have already been loaded.
To get better performance, we need a way to manage the decoded tile cache from the widget. For example, as we zoom in and out, tiles from the old zoom level should be released, newly needed tiles loaded, and useful tiles kept around. Increasing the shared cache size alone doesn't give the widget that control.
LVGL 9.2, for example, lets applications size the image cache in bytes and choose whether resizing immediately evicts entries with
lv_image_cache_resize(new_size, evict_now). EdgeTX currently uses LVGL 8.4.1, and I don't know of a planned upgrade to those APIs. This provides a way to manage image lifetimes with EdgeTX's existing LVGL version, without making that upgrade a prerequisite.Details
lvgl.image({bitmap=b})andimage:set({bitmap=b})display an existing Bitmap. Bitmap support is documented as added in 3.0.0, so widgets can detect it with a firmware version check and keep a filename-based fallback for older firmware.image:set({bitmap=false})clears it immediately, even while hidden. Settingbitmap=nildoes not clear it, since nil fields are absent from Lua tables.image:set({file = "..."})now changes the displayed image and replaces any previous filename callback. Previously the new path was stored but never applied, and an existing callback kept overriding it.The binding is generic: there is no map-specific API, change to LVGL, larger image cache or increased bitmap memory budget. The API documentation and 13 focused tests are included.
Performance experiments
I used a test widget to load a tiled map, drag it, zoom in and drag again, then zoom out and drag once more. Using the same v2.12.0-based native TX16S simulator build for both modes, the existing
fileAPI produced on the order of 100 JPEG decode events during those drag phases. Withbitmapsources, those drag-time decodes were completely eliminated; initial tile loading still required decoding.Validation
Validated on actual hardware using a custom v2.12.0-based build on my TX16S Mk2. Both the standalone Bitmap test and the AeroGrid map were responsive. The main-based revision also passes all 34 targeted native tests, but has not yet been tested on hardware.