[mipi_rgb] direct-to-PSRAM fast rotated blitting (#20905)

This commit is contained in:
Hasan Khalil
2026-10-17 10:39:44 +11:00
committed by GitHub
parent 1ddacb4931
commit b3f5789dc6
8 changed files with 170 additions and 44 deletions
+1 -1
View File
@@ -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,
+30 -20
View File
@@ -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<uint16_t *>(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<int>(this->width_) - x_start);
const int first_row = std::max(0, -y_start), end_row = std::min(h, static_cast<int>(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<size_t>(x_offset) + w + x_pad;
const auto *src_origin =
reinterpret_cast<const uint16_t *>(ptr) + (static_cast<size_t>(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);
}
}
+3 -18
View File
@@ -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 <esp_lcd_panel_rgb.h>
#include <utility>
#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
@@ -0,0 +1,76 @@
#pragma once
#include "esphome/components/display/display.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <utility>
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<size_t>(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<size_t>(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<size_t>(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
@@ -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}})
+5
View File
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.dependencies = manifest.dependencies + ["display"]
@@ -9,3 +9,4 @@ ch422g:
display:
- platform: mipi_rgb
model: WAVESHARE-5-1024X600
rotation: 90
@@ -0,0 +1,49 @@
#include <gtest/gtest.h>
#include <vector>
#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<uint16_t> 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<uint16_t> 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<uint16_t> 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