From b3f5789dc6e6da005a63b406961303015a8a17cf Mon Sep 17 00:00:00 2001 From: Hasan Khalil Date: Fri, 16 Oct 2026 19:39:44 -0400 Subject: [PATCH] [mipi_rgb] direct-to-PSRAM fast rotated blitting (#20905) --- esphome/components/mipi_rgb/display.py | 2 +- esphome/components/mipi_rgb/mipi_rgb.cpp | 50 +++++++----- esphome/components/mipi_rgb/mipi_rgb.h | 21 +---- .../components/mipi_rgb/mipi_rgb_blitter.h | 76 +++++++++++++++++++ .../mipi_rgb/test_mipi_rgb_config.py | 10 +-- tests/components/mipi_rgb/__init__.py | 5 ++ .../mipi_rgb/test.esp32-s3-idf.yaml | 1 + tests/components/mipi_rgb/test_blitter.cpp | 49 ++++++++++++ 8 files changed, 170 insertions(+), 44 deletions(-) create mode 100644 esphome/components/mipi_rgb/mipi_rgb_blitter.h create mode 100644 tests/components/mipi_rgb/__init__.py create mode 100644 tests/components/mipi_rgb/test_blitter.cpp diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 6e508dd7c44..e973d78ae4d 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -242,7 +242,7 @@ def _config_schema(config: ConfigType) -> ConfigType: config[CONF_ID], width, height, - model.rotation_as_transform(config), + has_hardware_rotation=True, byte_order=config[CONF_BYTE_ORDER], has_writer=requires_buffer(config) or config.get(CONF_AUTO_CLEAR_ENABLED) is True, diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index a38404030ad..beb39ce8473 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -175,6 +175,15 @@ void MipiRgb::common_setup_() { err = esp_lcd_panel_reset(this->handle_); if (err == ESP_OK) err = esp_lcd_panel_init(this->handle_); + if (err == ESP_OK) { + void *fb = nullptr; + err = esp_lcd_rgb_panel_get_frame_buffer(this->handle_, 1, &fb); + if (err == ESP_OK && fb != nullptr) { + this->fb_ = reinterpret_cast(fb); + } else if (err == ESP_OK) { + err = ESP_ERR_NO_MEM; + } + } if (err != ESP_OK) { ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err)); this->mark_failed(LOG_STR("lcd setup failed")); @@ -223,30 +232,31 @@ void MipiRgb::draw_pixels_internal(int x_start, int y_start, int w, int h, const int x_pad) { if (w <= 0 || h <= 0 || this->is_failed()) return; - if (bitness != display::COLOR_BITNESS_565 || big_endian != this->big_endian_ || - this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || this->is_clipping()) { + if (bitness != display::COLOR_BITNESS_565 || big_endian != this->big_endian_ || this->is_clipping()) { Display::draw_pixels_internal(x_start, y_start, w, h, ptr, bitness, big_endian, x_offset, y_offset, x_pad); this->mark_dirty_(x_start, y_start, w, h); this->flush_dirty_(); - } else if (this->buffer_ != nullptr) { - // With a buffer the block goes through it, or a flush of the old contents would paint over it - const int first_col = std::max(0, -x_start), end_col = std::min(w, static_cast(this->width_) - x_start); - const int first_row = std::max(0, -y_start), end_row = std::min(h, static_cast(this->height_) - y_start); - if (first_col >= end_col || first_row >= end_row) - return; - const size_t stride = x_offset + w + x_pad; - for (int y = first_row; y != end_row; y++) { - memcpy(this->buffer_ + (y_start + y) * this->width_ + x_start + first_col, - ptr + ((y_offset + y) * stride + x_offset + first_col) * 2, (end_col - first_col) * 2); + return; + } + + const int log_w = this->get_width(); + const int log_h = this->get_height(); + + if (x_start < 0 || y_start < 0 || x_start + w > log_w || y_start + h > log_h) { + return; + } + + uint16_t *dst = this->buffer_ != nullptr ? this->buffer_ : this->fb_; + const size_t stride = static_cast(x_offset) + w + x_pad; + const auto *src_origin = + reinterpret_cast(ptr) + (static_cast(y_offset) * stride + x_offset); + + blit_rotated_565(dst, this->width_, this->height_, this->rotation_, src_origin, x_start, y_start, w, h, stride); + if (dst == this->buffer_) { + this->mark_dirty_(x_start, y_start, w, h); + if (!this->updating_) { + this->flush_dirty_(); } - if (this->updating_) { - this->mark_dirty_(x_start + first_col, y_start + first_row, end_col - first_col, end_row - first_row); - } else { - this->write_to_display_(x_start + first_col, y_start + first_row, end_col - first_col, end_row - first_row, ptr, - x_offset + first_col, y_offset + first_row, x_pad + w - end_col); - } - } else { - this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); } } diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h index f6aec76976b..199dfcfbfc7 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.h +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -5,6 +5,7 @@ #include "esphome/core/log.h" #include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" +#include "mipi_rgb_blitter.h" #include #include #ifdef USE_SPI @@ -83,24 +84,7 @@ class MipiRgb : public display::Display { /// Send the region touched since the last flush to the panel. void flush_dirty_(); // Maps drawn coordinates to the buffer the way draw_pixel_at() stores them - void rotate_(int &x, int &y) const { - switch (this->rotation_) { - case display::DISPLAY_ROTATION_0_DEGREES: - break; - case display::DISPLAY_ROTATION_90_DEGREES: - std::swap(x, y); - x = this->width_ - x - 1; - break; - case display::DISPLAY_ROTATION_180_DEGREES: - x = this->width_ - x - 1; - y = this->height_ - y - 1; - break; - case display::DISPLAY_ROTATION_270_DEGREES: - std::swap(x, y); - y = this->height_ - y - 1; - break; - } - } + void rotate_(int &x, int &y) const { rotate_point(x, y, this->width_, this->height_, this->rotation_); } void mark_dirty_(int x_start, int y_start, int w, int h); void dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset); void setup_enables_(); @@ -134,6 +118,7 @@ class MipiRgb : public display::Display { uint16_t y_high_{0}; esp_lcd_panel_handle_t handle_{}; + uint16_t *fb_{nullptr}; }; #ifdef USE_SPI diff --git a/esphome/components/mipi_rgb/mipi_rgb_blitter.h b/esphome/components/mipi_rgb/mipi_rgb_blitter.h new file mode 100644 index 00000000000..1f64b40f5b6 --- /dev/null +++ b/esphome/components/mipi_rgb/mipi_rgb_blitter.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/components/display/display.h" +#include +#include +#include +#include +#include + +namespace esphome::mipi_rgb { + +inline void rotate_point(int &x, int &y, int width, int height, display::DisplayRotation rotation) { + switch (rotation) { + case display::DISPLAY_ROTATION_0_DEGREES: + break; + case display::DISPLAY_ROTATION_90_DEGREES: + std::swap(x, y); + x = width - x - 1; + break; + case display::DISPLAY_ROTATION_180_DEGREES: + x = width - x - 1; + y = height - y - 1; + break; + case display::DISPLAY_ROTATION_270_DEGREES: + std::swap(x, y); + y = height - y - 1; + break; + } +} + +inline void blit_rotated_565(uint16_t *fb, int fb_width, int fb_height, display::DisplayRotation rotation, + const uint16_t *src_origin, int x_start, int y_start, int w, int h, size_t stride) { + switch (rotation) { + case display::DISPLAY_ROTATION_0_DEGREES: { + for (int py = 0; py < h; py++) { + memcpy(&fb[(y_start + py) * fb_width + x_start], src_origin + static_cast(py) * stride, + w * sizeof(uint16_t)); + } + break; + } + case display::DISPLAY_ROTATION_90_DEGREES: { + const size_t x_dest_offset = fb_width - y_start - h; + for (int px = 0; px < w; px++) { + uint16_t *dest = &fb[(x_start + px) * fb_width + x_dest_offset]; + for (int py = 0; py < h; py++) { + *dest++ = src_origin[static_cast(h - 1 - py) * stride + px]; + } + } + break; + } + case display::DISPLAY_ROTATION_180_DEGREES: { + const size_t x_dest_offset = fb_width - x_start - w; + for (int py = 0; py < h; py++) { + uint16_t *dest = &fb[(fb_height - 1 - (y_start + py)) * fb_width + x_dest_offset]; + const uint16_t *src_row = src_origin + static_cast(py) * stride; + for (int px = 0; px < w; px++) { + *dest++ = src_row[w - 1 - px]; + } + } + break; + } + case display::DISPLAY_ROTATION_270_DEGREES: { + for (int px = 0; px < w; px++) { + uint16_t *dest = &fb[(fb_height - 1 - x_start - px) * fb_width + y_start]; + const uint16_t *s = src_origin + px; + for (int py = 0; py < h; py++) { + *dest++ = *s; + s += stride; + } + } + break; + } + } +} + +} // namespace esphome::mipi_rgb diff --git a/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py b/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py index 58e7ba9d959..96da849e3c4 100644 --- a/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py +++ b/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py @@ -139,14 +139,14 @@ def test_st7701s_only_supports_mirror_x( CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": True}}) -def test_pure_rgb_model_has_no_hardware_transform( +def test_pure_rgb_model_has_hardware_rotation( set_core_config: SetCoreConfigCallable, ) -> None: """A model with no init sequence has no command interface, so no transform - can be applied in hardware and every rotation must be done in software. + can be applied in panel silicon (MADCTL). - Previously such models claimed mirror_x/mirror_y support, so a rotation of - 180 was handed to the (nonexistent) hardware and the display was not rotated. + The driver itself handles rotation directly in the framebuffer without + allocating software rotation buffers, so has_hardware_rotation is True. """ _set_s3(set_core_config) @@ -162,7 +162,7 @@ def test_pure_rgb_model_has_no_hardware_transform( assert not model.rotation_as_transform(config) metadata = get_display_metadata(config["id"]) assert metadata.rotation == rotation - assert not metadata.has_hardware_rotation + assert metadata.has_hardware_rotation with pytest.raises(cv.Invalid, match="'mirror_x' is not supported by this model"): CONFIG_SCHEMA({**base, "transform": {"mirror_x": True}}) diff --git a/tests/components/mipi_rgb/__init__.py b/tests/components/mipi_rgb/__init__.py new file mode 100644 index 00000000000..584c6e74b42 --- /dev/null +++ b/tests/components/mipi_rgb/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.dependencies = manifest.dependencies + ["display"] diff --git a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml index 12b45ee160e..8a3a6fd3237 100644 --- a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml +++ b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml @@ -9,3 +9,4 @@ ch422g: display: - platform: mipi_rgb model: WAVESHARE-5-1024X600 + rotation: 90 diff --git a/tests/components/mipi_rgb/test_blitter.cpp b/tests/components/mipi_rgb/test_blitter.cpp new file mode 100644 index 00000000000..48da6d12199 --- /dev/null +++ b/tests/components/mipi_rgb/test_blitter.cpp @@ -0,0 +1,49 @@ +#include +#include +#include "esphome/components/mipi_rgb/mipi_rgb_blitter.h" + +namespace esphome::mipi_rgb::testing { + +static void test_rotation(display::DisplayRotation rotation) { + constexpr int fb_width = 40; + constexpr int fb_height = 30; + constexpr int w = 7, h = 5, x_offset = 2, y_offset = 3, x_pad = 4; + constexpr size_t stride = x_offset + w + x_pad; + constexpr size_t lines = y_offset + h; + + std::vector source(stride * lines); + uint16_t val = 0x1000; + for (size_t y = 0; y < lines; y++) { + for (size_t x = 0; x < stride; x++) { + source[y * stride + x] = val++; + } + } + + int x_start = 10; + int y_start = 8; + + // Run blit_rotated_565 + std::vector fb_actual(fb_width * fb_height, 0); + const uint16_t *src_origin = source.data() + (y_offset * stride + x_offset); + blit_rotated_565(fb_actual.data(), fb_width, fb_height, rotation, src_origin, x_start, y_start, w, h, stride); + + // Compute reference per-pixel using rotate_point + std::vector fb_expected(fb_width * fb_height, 0); + for (int py = 0; py < h; py++) { + for (int px = 0; px < w; px++) { + int rx = x_start + px; + int ry = y_start + py; + rotate_point(rx, ry, fb_width, fb_height, rotation); + fb_expected[ry * fb_width + rx] = src_origin[py * stride + px]; + } + } + + EXPECT_EQ(fb_actual, fb_expected); +} + +TEST(MipiRgbBlitter, Rotation0) { test_rotation(display::DISPLAY_ROTATION_0_DEGREES); } +TEST(MipiRgbBlitter, Rotation90) { test_rotation(display::DISPLAY_ROTATION_90_DEGREES); } +TEST(MipiRgbBlitter, Rotation180) { test_rotation(display::DISPLAY_ROTATION_180_DEGREES); } +TEST(MipiRgbBlitter, Rotation270) { test_rotation(display::DISPLAY_ROTATION_270_DEGREES); } + +} // namespace esphome::mipi_rgb::testing