Skip to content

Add PPA hardware-accelerated rotation for 90/270 degrees on ESP32-P4#14311

Closed
agillis wants to merge 131 commits intoesphome:devfrom
agillis:ppa_rotate_90
Closed

Add PPA hardware-accelerated rotation for 90/270 degrees on ESP32-P4#14311
agillis wants to merge 131 commits intoesphome:devfrom
agillis:ppa_rotate_90

Conversation

@agillis
Copy link
Contributor

@agillis agillis commented Feb 26, 2026

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.

⚠️ Depends on [lvgl] Migrate to library v9.4.0 #12312 being merged first. This PR builds on top of the LVGL 9.4 upgrade.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • [ x ] New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Developer breaking change (an API change that could break external components)
  • Code quality improvements to existing code or addition of tests
  • Other

Related issue or feature (if applicable):

  • fixes

Pull request in esphome-docs with documentation (if applicable):

  • esphome/esphome-docs#

Test Environment

  • [ x ] ESP32
  • ESP32 IDF
  • ESP8266
  • RP2040
  • BK72xx
  • RTL87xx
  • LN882x
  • nRF52840

Example entry for config.yaml:

# Example config.yaml

Checklist:

  • The code change is tested and works locally.
  • Tests have been added to verify that the new code works (under tests/ folder).

If user exposed functionality or configuration variables are added/changed:

clydebarrow and others added 12 commits January 7, 2026 15:02
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.
Copilot AI review requested due to automatic review settings February 26, 2026 13:19
@agillis agillis requested review from a team and clydebarrow as code owners February 26, 2026 13:19
@github-actions
Copy link
Contributor

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)

@github-actions
Copy link
Contributor

👋 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.

Copy link

@esphome esphome bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📦 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

@esphome
Copy link

esphome bot commented Feb 26, 2026

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +680 to +686
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
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 45 to +52
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)))
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:.

Copilot uses AI. Check for mistakes.
Comment on lines 152 to 155
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:
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +591 to +599
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)
)
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
dst[y * width + x] = *ptr++;
y1 = x1;
x1 = this->height_ - area->y1 - height;
width = height;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
width = height;

Copilot uses AI. Check for mistakes.
Comment on lines +263 to +266
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));
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
w = Widget(var, wtype, config)
if name is not None:
widget_map[name] = w
widget_map[name] = w
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
widget_map[name] = w
if name is not None:
widget_map[name] = w

Copilot uses AI. Check for mistakes.
@agillis
Copy link
Contributor Author

agillis commented Feb 26, 2026

Closing this PR in favor of clydebarrow#140 which targets clydebarrow:lvgl-9.4 directly (PR #12312). This avoids showing the entire LVGL 9.4 migration diff.

@agillis agillis closed this Feb 26, 2026
@github-actions github-actions bot locked and limited conversation to collaborators Feb 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants