mirror of
https://github.com/esphome/esphome.git
synced 2026-10-17 11:42:51 +00:00
[mipi_rgb] direct-to-PSRAM fast rotated blitting (#20905)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}})
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user