Add PPA hardware-accelerated rotation for 90/270 degrees on ESP32-P4#14311
Add PPA hardware-accelerated rotation for 90/270 degrees on ESP32-P4#14311agillis wants to merge 131 commits intoesphome:devfrom
Conversation
…into lvgl-button
Add hardware-accelerated rotation using the ESP32-P4 PPA (Pixel Processing Accelerator) engine instead of software rotation. Falls back to software rotation on non-P4 platforms or if PPA registration fails. Key changes: - Use PPA SRM (Scale-Rotate-Mirror) operations for 90/180/270 degree rotation - Allocate DMA-compatible cache-line-aligned buffers for PPA compatibility - Register PPA client during setup with automatic fallback to software rotation
180-degree rotation can be handled natively by the display driver, so PPA hardware acceleration is only needed for 90 and 270 degrees.
|
To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file: external_components:
- source: github://pr#14311
components: [font, image, lvgl]
refresh: 1h(Added by the PR bot) |
|
👋 Hi there! This PR modifies 19 file(s) with codeowners. @clydebarrow, @esphome/core - As codeowner(s) of the affected files, your review would be appreciated! 🙏 Note: Automatic review request may have failed, but you're still welcome to review. |
There was a problem hiding this comment.
📦 Pull Request Size
This PR is too large with 1053 line changes (excluding tests). Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.
For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
There was a problem hiding this comment.
Pull request overview
Updates ESPHome’s LVGL integration to work with LVGL 9.4 APIs while adding ESP32-P4 PPA-assisted 90°/270° rotation to reduce CPU overhead during flush/rotate.
Changes:
- Bump LVGL dependency to 9.4.0 and migrate widget/component APIs accordingly.
- Rework LVGL core display/flush/rotation pipeline, including optional ESP32-P4 PPA hardware rotation.
- Update multiple widget implementations/schemas (meter→scale migration, msgbox, canvas, buttonmatrix, etc.) for LVGL 9 behavior.
Reviewed changes
Copilot reviewed 48 out of 48 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/components/lvgl/lvgl-package.yaml | Adjust LVGL test config for v9 compatibility. |
| platformio.ini | Bump LVGL library to 9.4.0. |
| esphome/components/lvgl/widgets/tileview.py | Update tile direction/position handling. |
| esphome/components/lvgl/widgets/tabview.py | Migrate tabview API + styling of tab bar/items. |
| esphome/components/lvgl/widgets/spinner.py | Update spinner schema + animation params. |
| esphome/components/lvgl/widgets/slider.py | Switch slider mode constants for LVGL 9. |
| esphome/components/lvgl/widgets/qrcode.py | Update qrcode schema + update path. |
| esphome/components/lvgl/widgets/obj.py | Import adjustments for refactored WidgetType. |
| esphome/components/lvgl/widgets/msgbox.py | Rework msgbox construction/buttons for LVGL 9. |
| esphome/components/lvgl/widgets/meter.py | Replace removed meter with scale-based implementation. |
| esphome/components/lvgl/widgets/lv_bar.py | Import adjustments for NumberType refactor. |
| esphome/components/lvgl/widgets/line.py | Update lambda coord handling. |
| esphome/components/lvgl/widgets/led.py | Update brightness validation to percentage. |
| esphome/components/lvgl/widgets/label.py | Import adjustments for WidgetType refactor. |
| esphome/components/lvgl/widgets/img.py | Migrate image properties (rotation/scale) for LVGL 9. |
| esphome/components/lvgl/widgets/container.py | Align container lv_name + remove old style stripping hook. |
| esphome/components/lvgl/widgets/canvas.py | Port canvas buffer + drawing actions to LVGL 9 layer API. |
| esphome/components/lvgl/widgets/buttonmatrix.py | Rename APIs/constants for buttonmatrix in LVGL 9. |
| esphome/components/lvgl/widgets/button.py | Update button type + creation hook for LVGL 9. |
| esphome/components/lvgl/widgets/arc.py | Refactor arc property mapping + flags/parts for LVGL 9. |
| esphome/components/lvgl/widgets/init.py | Major Widget/WidgetType refactor + update action auto-registration. |
| esphome/components/lvgl/types.py | Move WidgetType out, update LVGL types/constants. |
| esphome/components/lvgl/trigger.py | Update active-screen helper name. |
| esphome/components/lvgl/touchscreens.py | Adjust input device registration for LVGL 9. |
| esphome/components/lvgl/text/lvgl_text.h | Update text control lambda signature. |
| esphome/components/lvgl/styles.py | Add LVStyle helper + remap_property usage. |
| esphome/components/lvgl/select/lvgl_select.h | LVGL 9 event user_data API updates + include fix. |
| esphome/components/lvgl/schemas.py | Add property remapping + new style props for LVGL 9. |
| esphome/components/lvgl/number/init.py | Update numeric bounds access after Widget refactor. |
| esphome/components/lvgl/lvgl_hal.h | Remove old LVGL 8 custom HAL header. |
| esphome/components/lvgl/lvgl_esphome.h | Update LVGL 9 display/input APIs + rotation/PPA fields. |
| esphome/components/lvgl/lvgl_esphome.cpp | LVGL 9 display setup + flush/rotate (incl. ESP32-P4 PPA). |
| esphome/components/lvgl/lvcode.py | Add LVGL 9 attribute remapping + lambda context helpers. |
| esphome/components/lvgl/lv_validation.py | Update validators (opacity/scale/pct) for LVGL 9 types. |
| esphome/components/lvgl/light/lvgl_light.h | Replace lv_event_send with lv_obj_send_event. |
| esphome/components/lvgl/keypads.py | Adjust indev/group setup for LVGL 9. |
| esphome/components/lvgl/gradient.py | Rework gradients for LVGL 9 stop arrays + warnings. |
| esphome/components/lvgl/encoders.py | Adjust indev/group setup for LVGL 9. |
| esphome/components/lvgl/defines.py | Add warnings/remap tracking + StaticCastExpression utilities. |
| esphome/components/lvgl/automation.py | Add top/bottom layer support + deprecate disp_bg_* updates. |
| esphome/components/lvgl/init.py | Update LVGL compile defines, layer wiring, byte order handling. |
| esphome/components/image/image.h | LVGL descriptor API rename for LVGL 9. |
| esphome/components/image/image.cpp | Update LVGL image descriptor fields/formats for LVGL 9. |
| esphome/components/image/init.py | Add encoder end_image + LVGL-driven byte-order final validation. |
| esphome/components/font/font.h | Update LVGL font bitmap callback signature for LVGL 9. |
| esphome/components/font/font.cpp | Implement LVGL 9 glyph bitmap conversion into A8 draw buffer. |
| esphome/components/font/init.py | Define USE_FONT for LVGL header gating. |
| .clang-tidy.hash | Update clang-tidy hash after changes. |
| fv = full_config.get() | ||
| if "lvgl" in fv and not all(x.get(CONF_BYTE_ORDER) in x for x in config): | ||
| config = config.copy() | ||
| for c in config: | ||
| if not c.get(CONF_BYTE_ORDER): | ||
| c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN" | ||
| return config |
There was a problem hiding this comment.
The all(...) check is wrong: x.get(CONF_BYTE_ORDER) in x tests whether the value of byte_order is a key in the dict, not whether the key exists. This makes the condition always/usually true and defeats the intent. Use CONF_BYTE_ORDER in x (or x.get(CONF_BYTE_ORDER) is not None) to detect missing byte-order settings reliably.
| async def to_code(self, w: Widget, config): | ||
| await w.set_property( | ||
| CONF_LIGHT_COLOR, await lv_color.process(config.get(CONF_LIGHT_COLOR)) | ||
| ) | ||
| await w.set_property( | ||
| CONF_DARK_COLOR, await lv_color.process(config.get(CONF_DARK_COLOR)) | ||
| ) | ||
| await w.set_property(CONF_SIZE, await lv_int.process(config.get(CONF_SIZE))) |
There was a problem hiding this comment.
to_code() eagerly calls lv_color.process() / lv_int.process() on config.get(...) values. When the key is absent, these processors will receive None and can raise during codegen. Prefer calling w.set_property(..., config, processor=...) (so set_property short-circuits missing keys) or gate each process call with if CONF_* in config:.
| async with LambdaContext(EVENT_ARG, where=messagebox_id) as close_action: | ||
| outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") | ||
| outer_widget.add_flag(LV_OBJ_FLAG.HIDDEN) | ||
| outer_widget.set_style("bg_color", await lv_color.process("red")) | ||
| if close_button: |
There was a problem hiding this comment.
The close-action lambda sets the outer widget background to red on close. This looks like leftover debug behavior and will unexpectedly mutate styling every time the msgbox is dismissed. Remove the bg_color change (or make it conditional/configurable) and keep the close action focused on hiding/removing the msgbox.
| if indicator.type is arc_indicator_type: | ||
| if start_value is not None: | ||
| lv.arc_set_start_angle( | ||
| indicator.obj, lv.get_needle_angle_for_value(indicator.obj, start_value) | ||
| ) | ||
| if end_value is not None: | ||
| lv.arc_set_end_angle( | ||
| indicator.obj, lv.get_needle_angle_for_value(indicator.obj, start_value) | ||
| ) |
There was a problem hiding this comment.
lv.arc_set_end_angle() is computed using start_value instead of end_value, so arc indicators will render an incorrect range whenever end_value is provided. Use end_value when calculating the end angle.
| dst[y * width + x] = *ptr++; | ||
| y1 = x1; | ||
| x1 = this->height_ - area->y1 - height; | ||
| width = height; |
There was a problem hiding this comment.
In the 90° software rotation path, width/height are reassigned incorrectly (width = height; height = width;), so the resulting dimensions will be wrong (height becomes the original height, not the original width). Fix the swap logic to match the intended rotated buffer dimensions (i.e., height should become the original width, and width should become height_rounded).
| width = height; |
| esp_err_t err = ppa_do_scale_rotate_mirror(static_cast<ppa_client_handle_t>(this->ppa_client_), &srm); | ||
| if (err != ESP_OK) { | ||
| ESP_LOGE(TAG, "PPA rotation failed: %s", esp_err_to_name(err)); | ||
| } |
There was a problem hiding this comment.
If ppa_do_scale_rotate_mirror() fails, the code logs an error but still proceeds to draw from dst, which may contain partially-written or stale data. Consider falling back to the software rotation path on error (or at least drawing from the unrotated buffer) to avoid corrupt frames.
| w = Widget(var, wtype, config) | ||
| if name is not None: | ||
| widget_map[name] = w | ||
| widget_map[name] = w |
There was a problem hiding this comment.
Widget.create() now unconditionally stores every widget in widget_map, even when name is None. Some internal widget helpers (e.g. creating a button's child label) pass None, so this will create/overwrite a None entry in the map and can lead to unexpected lookups/iteration behavior. Restore the if name is not None: guard (or require an explicit ID for map insertion).
| widget_map[name] = w | |
| if name is not None: | |
| widget_map[name] = w |
|
Closing this PR in favor of clydebarrow#140 which targets |
What does this implement/fix?
Add PPA hardware-accelerated rotation for 90/270 degrees on ESP32-P4
Uses the ESP32-P4 PPA (Pixel Processing Accelerator) for screen rotation instead of software rotation. 180-degree rotation is left to the display driver since it handles it natively.
Types of changes
Related issue or feature (if applicable):
Pull request in esphome-docs with documentation (if applicable):
Test Environment
Example entry for
config.yaml:# Example config.yamlChecklist:
tests/folder).If user exposed functionality or configuration variables are added/changed: