diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index c1389721496..f994d89555b 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -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. */ diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index 8e2fd78e621..86dffdad1f2 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -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: diff --git a/esphome/components/hub75/hub75_component.h b/esphome/components/hub75/hub75_component.h index 98bc2e52e6f..a69a06596b7 100644 --- a/esphome/components/hub75/hub75_component.h +++ b/esphome/components/hub75/hub75_component.h @@ -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; diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index e5d31c81d8c..cb0d752aa00 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -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_(x, y, display, color_on, color_off, img_x0, img_y0, w, h); - } else { - this->draw_(x, y, display, color_on, color_off, img_x0, img_y0, w, h); - } -} - -template -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_(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end); + } else { + this->draw_(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_(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end); + } else { + this->draw_(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_(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end); + } else { + this->draw_(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_(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end); + } else { + this->draw_(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end); } break; } } + +template +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; diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h index 6210c4a7374..9ffbbfff84e 100644 --- a/esphome/components/image/image.h +++ b/esphome/components/image/image.h @@ -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 - 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 + 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; diff --git a/esphome/components/it8951/it8951.h b/esphome/components/it8951/it8951.h index a5ed03e8c4b..7db6224e9bb 100644 --- a/esphome/components/it8951/it8951.h +++ b/esphome/components/it8951/it8951.h @@ -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_; } diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 550e1998bb5..4bf3e2fb9b7 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -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)) diff --git a/tests/benchmarks/components/image/bench_image_draw.cpp b/tests/benchmarks/components/image/bench_image_draw.cpp index 1cdad32ba71..50e672f2a16 100644 --- a/tests/benchmarks/components/image/bench_image_draw.cpp +++ b/tests/benchmarks/components/image/bench_image_draw.cpp @@ -16,14 +16,14 @@ static constexpr int kHeight = 96; // buffered display shares. class BenchDisplay : public display::DisplayBuffer { public: - BenchDisplay() : frame_(std::make_unique(kWidth * kHeight)) { this->buffer_ = this->frame_.get(); } + BenchDisplay() : frame_(std::make_unique(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 frame_; };