mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
[image][display] Let the display say whether pixel writes swap the axes
Rotation alone is the wrong signal: mipi_spi with hardware rotation, epaper_spi and it8951 with a swap_xy transform, and hub75 write the frame buffer differently from what the rotation implies. The display now answers through pixel_axes_swapped() and Image::draw() picks the loop order from that. The four per-format loops collapse into one body with the format resolved at compile time.
This commit is contained in:
@@ -710,6 +710,13 @@ class Display : public PollingComponent {
|
||||
|
||||
DisplayRotation get_rotation() const { return this->rotation_; }
|
||||
|
||||
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
|
||||
/// neighbours along x land a row apart in the frame buffer. Callers drawing
|
||||
/// many pixels use it to pick the loop order that keeps the writes sequential.
|
||||
virtual bool pixel_axes_swapped() const {
|
||||
return this->rotation_ == DISPLAY_ROTATION_90_DEGREES || this->rotation_ == DISPLAY_ROTATION_270_DEGREES;
|
||||
}
|
||||
|
||||
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
|
||||
* returns the type the display is currently configured to.
|
||||
*/
|
||||
|
||||
@@ -113,6 +113,7 @@ class EPaperBase : public Display,
|
||||
|
||||
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
|
||||
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -31,6 +31,8 @@ class HUB75Display final : public display::Display {
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
void fill(Color color) override;
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
// Rotation is not applied to pixel writes.
|
||||
bool pixel_axes_swapped() const override { return false; }
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
|
||||
|
||||
@@ -6,112 +6,110 @@
|
||||
namespace esphome::image {
|
||||
|
||||
void Image::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
|
||||
int img_x0 = 0;
|
||||
int img_y0 = 0;
|
||||
int w = width_;
|
||||
int h = height_;
|
||||
int x_start = 0;
|
||||
int y_start = 0;
|
||||
int x_end = width_;
|
||||
int y_end = height_;
|
||||
|
||||
auto clipping = display->get_clipping();
|
||||
if (clipping.is_set()) {
|
||||
if (clipping.x > x)
|
||||
img_x0 += clipping.x - x;
|
||||
x_start += clipping.x - x;
|
||||
if (clipping.y > y)
|
||||
img_y0 += clipping.y - y;
|
||||
if (w > clipping.x2() - x)
|
||||
w = clipping.x2() - x;
|
||||
if (h > clipping.y2() - y)
|
||||
h = clipping.y2() - y;
|
||||
y_start += clipping.y - y;
|
||||
if (x_end > clipping.x2() - x)
|
||||
x_end = clipping.x2() - x;
|
||||
if (y_end > clipping.y2() - y)
|
||||
y_end = clipping.y2() - y;
|
||||
}
|
||||
|
||||
// Pixel data and frame buffers are row-major, so walking rows keeps both the
|
||||
// image read and the frame buffer write sequential. A rotated display swaps
|
||||
// x and y before the write, so there the columns are walked to keep the
|
||||
// write sequential; a strided write costs more than a strided read.
|
||||
const auto rotation = display->get_rotation();
|
||||
if (rotation == display::DISPLAY_ROTATION_0_DEGREES || rotation == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||
this->draw_<true>(x, y, display, color_on, color_off, img_x0, img_y0, w, h);
|
||||
} else {
|
||||
this->draw_<false>(x, y, display, color_on, color_off, img_x0, img_y0, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
template<bool ROWS_OUTER>
|
||||
void Image::draw_(int x, int y, display::Display *display, Color color_on, Color color_off, int img_x0, int img_y0,
|
||||
int w, int h) {
|
||||
// The outer loop runs over rows or columns; the selects fold at compile time.
|
||||
const int outer0 = ROWS_OUTER ? img_y0 : img_x0;
|
||||
const int outer_end = ROWS_OUTER ? h : w;
|
||||
const int inner0 = ROWS_OUTER ? img_x0 : img_y0;
|
||||
const int inner_end = ROWS_OUTER ? w : h;
|
||||
|
||||
// Pixel data and frame buffers are row-major, so walking rows keeps the
|
||||
// image read and the frame buffer write sequential. When the display swaps
|
||||
// the axes before writing, walking columns keeps the write sequential
|
||||
// instead; a strided write costs more than a strided read.
|
||||
const bool rows_outer = !display->pixel_axes_swapped();
|
||||
switch (type_) {
|
||||
case IMAGE_TYPE_BINARY: {
|
||||
for (int outer = outer0; outer < outer_end; outer++) {
|
||||
for (int inner = inner0; inner < inner_end; inner++) {
|
||||
const int img_x = ROWS_OUTER ? inner : outer;
|
||||
const int img_y = ROWS_OUTER ? outer : inner;
|
||||
if (this->get_binary_pixel_(img_x, img_y)) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_on);
|
||||
} else if (!this->transparency_) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_off);
|
||||
}
|
||||
}
|
||||
case IMAGE_TYPE_BINARY:
|
||||
if (rows_outer) {
|
||||
this->draw_<true, IMAGE_TYPE_BINARY>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
} else {
|
||||
this->draw_<false, IMAGE_TYPE_BINARY>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
for (int outer = outer0; outer < outer_end; outer++) {
|
||||
for (int inner = inner0; inner < inner_end; inner++) {
|
||||
const int img_x = ROWS_OUTER ? inner : outer;
|
||||
const int img_y = ROWS_OUTER ? outer : inner;
|
||||
const uint32_t pos = (img_x + img_y * this->width_);
|
||||
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
||||
Color color = Color(gray, gray, gray, 0xFF);
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
if (gray == 1) {
|
||||
continue; // skip drawing
|
||||
}
|
||||
break;
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
// gray is the alpha: blend from color_off to color_on, drawn opaque
|
||||
color = Color(Color::blend_channel(color_off.r, color_on.r, gray),
|
||||
Color::blend_channel(color_off.g, color_on.g, gray),
|
||||
Color::blend_channel(color_off.b, color_on.b, gray), 0xFF);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
if (rows_outer) {
|
||||
this->draw_<true, IMAGE_TYPE_GRAYSCALE>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
} else {
|
||||
this->draw_<false, IMAGE_TYPE_GRAYSCALE>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGB565:
|
||||
for (int outer = outer0; outer < outer_end; outer++) {
|
||||
for (int inner = inner0; inner < inner_end; inner++) {
|
||||
const int img_x = ROWS_OUTER ? inner : outer;
|
||||
const int img_y = ROWS_OUTER ? outer : inner;
|
||||
auto color = this->get_rgb565_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
if (rows_outer) {
|
||||
this->draw_<true, IMAGE_TYPE_RGB565>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
} else {
|
||||
this->draw_<false, IMAGE_TYPE_RGB565>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGB:
|
||||
for (int outer = outer0; outer < outer_end; outer++) {
|
||||
for (int inner = inner0; inner < inner_end; inner++) {
|
||||
const int img_x = ROWS_OUTER ? inner : outer;
|
||||
const int img_y = ROWS_OUTER ? outer : inner;
|
||||
auto color = this->get_rgb_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
if (rows_outer) {
|
||||
this->draw_<true, IMAGE_TYPE_RGB>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
} else {
|
||||
this->draw_<false, IMAGE_TYPE_RGB>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
template<bool ROWS_OUTER, ImageType TYPE>
|
||||
void Image::draw_(int x, int y, display::Display *display, Color color_on, Color color_off, int x_start, int x_end,
|
||||
int y_start, int y_end) {
|
||||
// The selects below fold at compile time, so each instantiation is a plain
|
||||
// nested loop with one pixel format inside.
|
||||
const int outer_start = ROWS_OUTER ? y_start : x_start;
|
||||
const int outer_end = ROWS_OUTER ? y_end : x_end;
|
||||
const int inner_start = ROWS_OUTER ? x_start : y_start;
|
||||
const int inner_end = ROWS_OUTER ? x_end : y_end;
|
||||
|
||||
for (int outer = outer_start; outer < outer_end; outer++) {
|
||||
for (int inner = inner_start; inner < inner_end; inner++) {
|
||||
const int img_x = ROWS_OUTER ? inner : outer;
|
||||
const int img_y = ROWS_OUTER ? outer : inner;
|
||||
if constexpr (TYPE == IMAGE_TYPE_BINARY) {
|
||||
if (this->get_binary_pixel_(img_x, img_y)) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_on);
|
||||
} else if (!this->transparency_) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_off);
|
||||
}
|
||||
} else if constexpr (TYPE == IMAGE_TYPE_GRAYSCALE) {
|
||||
const uint32_t pos = (img_x + img_y * this->width_);
|
||||
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
||||
Color color = Color(gray, gray, gray, 0xFF);
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
if (gray == 1) {
|
||||
continue; // skip drawing
|
||||
}
|
||||
break;
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
// gray is the alpha: blend from color_off to color_on, drawn opaque
|
||||
color = Color(Color::blend_channel(color_off.r, color_on.r, gray),
|
||||
Color::blend_channel(color_off.g, color_on.g, gray),
|
||||
Color::blend_channel(color_off.b, color_on.b, gray), 0xFF);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
} else {
|
||||
auto color =
|
||||
TYPE == IMAGE_TYPE_RGB565 ? this->get_rgb565_pixel_(img_x, img_y) : this->get_rgb_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return color_off;
|
||||
|
||||
@@ -43,10 +43,11 @@ class Image : public display::BaseImage {
|
||||
lv_image_dsc_t *get_lv_image_dsc();
|
||||
#endif
|
||||
protected:
|
||||
/// Per-pixel draw with rows (true) or columns (false) as the outer loop.
|
||||
template<bool ROWS_OUTER>
|
||||
void draw_(int x, int y, display::Display *display, Color color_on, Color color_off, int img_x0, int img_y0, int w,
|
||||
int h);
|
||||
/// Per-pixel draw of the clipped image range with rows (true) or columns
|
||||
/// (false) as the outer loop; the pixel format is resolved at compile time.
|
||||
template<bool ROWS_OUTER, ImageType TYPE>
|
||||
void draw_(int x, int y, display::Display *display, Color color_on, Color color_off, int x_start, int x_end,
|
||||
int y_start, int y_end);
|
||||
bool get_binary_pixel_(int x, int y) const;
|
||||
Color get_rgb_pixel_(int x, int y) const;
|
||||
Color get_rgb565_pixel_(int x, int y) const;
|
||||
|
||||
@@ -204,6 +204,7 @@ class IT8951Display : public Display,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
|
||||
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & TRANSFORM_SWAP_XY; }
|
||||
|
||||
protected:
|
||||
int get_height_internal() override { return this->height_; }
|
||||
|
||||
@@ -602,6 +602,9 @@ class MipiSpiBuffer
|
||||
#endif
|
||||
}
|
||||
|
||||
// With hardware rotation the buffer is written in drawn coordinates.
|
||||
bool pixel_axes_swapped() const override { return !HAS_HARDWARE_ROTATION && display::Display::pixel_axes_swapped(); }
|
||||
|
||||
// Draw a pixel at the given coordinates.
|
||||
void draw_pixel_at(int x, int y, Color color) override {
|
||||
if (this->is_point_clipped(x, y))
|
||||
|
||||
@@ -16,14 +16,14 @@ static constexpr int kHeight = 96;
|
||||
// buffered display shares.
|
||||
class BenchDisplay : public display::DisplayBuffer {
|
||||
public:
|
||||
BenchDisplay() : frame_(std::make_unique<uint8_t[]>(kWidth * kHeight)) { this->buffer_ = this->frame_.get(); }
|
||||
BenchDisplay() : frame_(std::make_unique<uint8_t[]>(kWidth * kHeight)) {}
|
||||
void update() override {}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
int get_width_internal() override { return kWidth; }
|
||||
int get_height_internal() override { return kHeight; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override { this->buffer_[y * kWidth + x] = color.r; }
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override { this->frame_[y * kWidth + x] = color.r; }
|
||||
|
||||
std::unique_ptr<uint8_t[]> frame_;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user