mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d92fdfa24b | ||
|
|
2da55e1f65 | ||
|
|
30e8b8379c | ||
|
|
1203e6009a | ||
|
|
a35fcc12c3 | ||
|
|
2b894cd255 | ||
|
|
eab2adc9f3 | ||
|
|
76b6251c6c | ||
|
|
cdfcb71f58 | ||
|
|
e3a62194e5 | ||
|
|
bf5e74120f | ||
|
|
d67a78d53a | ||
|
|
24edcbc1e1 | ||
|
|
3a460d950e | ||
|
|
1c2325da30 | ||
|
|
e41d3511ed | ||
|
|
eaec226add | ||
|
|
3dea35814c | ||
|
|
4d130624b4 | ||
|
|
de93f2e9ff |
@@ -5,8 +5,8 @@
|
||||
namespace esphome::animation {
|
||||
|
||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
|
||||
image::ImageType type, image::Transparency transparent)
|
||||
: Image(data_start, width, height, type, transparent),
|
||||
image::ImageType type, image::Transparency transparent, bool big_endian)
|
||||
: Image(data_start, width, height, type, transparent, big_endian),
|
||||
animation_data_start_(data_start),
|
||||
current_frame_(0),
|
||||
animation_frame_count_(animation_frame_count),
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace esphome::animation {
|
||||
class Animation final : public image::Image {
|
||||
public:
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type,
|
||||
image::Transparency transparent);
|
||||
image::Transparency transparent, bool big_endian = false);
|
||||
|
||||
uint32_t get_animation_frame_count() const;
|
||||
int get_current_frame() const;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_LOOP
|
||||
from esphome.components.const import CONF_BYTE_ORDER, CONF_LOOP
|
||||
from esphome.components.file import image as file_image
|
||||
from esphome.components.file.image import image_schema, write_image
|
||||
from esphome.components.image import Image_, validate_settings
|
||||
@@ -114,6 +114,7 @@ async def setup_animation(config: ConfigType) -> None:
|
||||
frame_count,
|
||||
image_type,
|
||||
trans_value,
|
||||
config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN",
|
||||
)
|
||||
if loop_config := config.get(CONF_LOOP):
|
||||
start = loop_config[CONF_START_FRAME]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::display {
|
||||
@@ -52,40 +53,65 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
|
||||
this->line(x1, y1, x2, y2, color);
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
uint32_t color_value;
|
||||
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
|
||||
// -Os stops inlining it once there are two call sites, which costs a call and
|
||||
// spills per pixel. Without PSRAM there is one call site and it changes
|
||||
// nothing, and draw_pixel_at() stays a virtual call either way.
|
||||
template<ColorBitness BITNESS>
|
||||
static void __attribute__((flatten))
|
||||
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
// BITNESS is a constant here, so to_color()'s three per pixel divisions
|
||||
// become a multiply and shift and its bitness switch folds.
|
||||
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
|
||||
display->draw_pixel_at(
|
||||
x + x_start, y + y_start,
|
||||
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
|
||||
};
|
||||
// The source and frame buffers are row-major, so walking rows keeps the read
|
||||
// and the write sequential. When the display swaps the axes before writing,
|
||||
// walking columns keeps the write sequential instead; with the frame buffer
|
||||
// in PSRAM a strided write costs more than a strided read.
|
||||
#ifdef USE_PSRAM
|
||||
if (display->pixel_axes_swapped()) {
|
||||
size_t column_idx = y_offset * line_stride + x_offset;
|
||||
for (int x = 0; x != w; x++, column_idx++) {
|
||||
size_t source_idx = column_idx;
|
||||
for (int y = 0; y != h; y++, source_idx += line_stride) {
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
|
||||
for (int y = 0; y != h; y++) {
|
||||
size_t source_idx = (y_offset + y) * line_stride + x_offset;
|
||||
size_t source_idx_mod;
|
||||
for (int x = 0; x != w; x++, source_idx++) {
|
||||
switch (bitness) {
|
||||
default:
|
||||
color_value = ptr[source_idx];
|
||||
break;
|
||||
case COLOR_BITNESS_565:
|
||||
source_idx_mod = source_idx * 2;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
|
||||
}
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
source_idx_mod = source_idx * 3;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
switch (bitness) {
|
||||
case COLOR_BITNESS_565:
|
||||
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
default:
|
||||
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
|
||||
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
|
||||
for (int i = x; i < x + width; i++)
|
||||
|
||||
@@ -137,6 +137,11 @@ enum DisplayRotation {
|
||||
DISPLAY_ROTATION_270_DEGREES = 270,
|
||||
};
|
||||
|
||||
/// Whether a rotation swaps the x and y axes.
|
||||
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
|
||||
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
|
||||
}
|
||||
|
||||
const int EDGES_TRIGON = 3;
|
||||
const int EDGES_TRIANGLE = 3;
|
||||
const int EDGES_TETRAGON = 4;
|
||||
@@ -355,6 +360,11 @@ class Display : public PollingComponent {
|
||||
* \param x_pad How many pixels are in each line after the end of the pixels to be copied.
|
||||
*
|
||||
* The length of each source buffer line (stride) will be x_offset + w + x_pad.
|
||||
*
|
||||
* The destination is in drawn coordinates, so an override that copies the block straight into
|
||||
* its buffer must hand anything it cannot copy as is (another pixel format or byte order, a
|
||||
* software rotation, an active clipping rectangle) to this base implementation, which goes
|
||||
* through draw_pixel_at(). Callers keep the block on screen.
|
||||
*/
|
||||
virtual void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad);
|
||||
@@ -710,6 +720,12 @@ 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. Not the same as
|
||||
/// get_width()/get_height() swapping: a display that rotates in hardware
|
||||
/// reports swapped dimensions but writes unswapped pixels.
|
||||
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
|
||||
|
||||
/** 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.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::display {
|
||||
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
|
||||
@@ -8,6 +9,27 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
|
||||
|
||||
class ColorUtil {
|
||||
public:
|
||||
/// Reads one packed pixel of the given bitness from a source buffer into the
|
||||
/// value to_color() takes. BITNESS is a template argument so the byte layout
|
||||
/// folds; pass it on to to_color() as a constant too, and its channel scaling
|
||||
/// divides by a constant, which the compiler turns into a multiply and shift.
|
||||
template<ColorBitness BITNESS>
|
||||
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
|
||||
if constexpr (BITNESS == COLOR_BITNESS_565) {
|
||||
const size_t idx = index * 2;
|
||||
if (big_endian)
|
||||
return (ptr[idx] << 8) + ptr[idx + 1];
|
||||
return ptr[idx] + (ptr[idx + 1] << 8);
|
||||
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
|
||||
const size_t idx = index * 3;
|
||||
if (big_endian)
|
||||
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
|
||||
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
|
||||
} else {
|
||||
return ptr[index];
|
||||
}
|
||||
}
|
||||
|
||||
static Color to_color(uint32_t colorcode, ColorOrder color_order,
|
||||
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
|
||||
uint8_t first_color, second_color, third_color;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -254,7 +254,13 @@ async def new_image(config: ConfigType) -> MockObj:
|
||||
"""
|
||||
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID], prog_arr, width, height, image_type, trans_value
|
||||
config[CONF_ID],
|
||||
prog_arr,
|
||||
width,
|
||||
height,
|
||||
image_type,
|
||||
trans_value,
|
||||
config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN",
|
||||
)
|
||||
add_metadata(
|
||||
config[CONF_ID], width, height, config[CONF_TYPE], config[CONF_TRANSPARENCY]
|
||||
|
||||
@@ -1,85 +1,135 @@
|
||||
#include "image.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::image {
|
||||
|
||||
// Walks the clipped image range with rows (ROWS_OUTER) or columns as the outer
|
||||
// loop. The coordinate selection folds at compile time, and the pixel body must
|
||||
// be ESPHOME_ALWAYS_INLINE: at -Os GCC otherwise leaves it out of line and calls
|
||||
// it once per pixel.
|
||||
template<bool ROWS_OUTER, typename F>
|
||||
static void for_each_pixel(int x_start, int x_end, int y_start, int y_end, F &&f) {
|
||||
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++) {
|
||||
f(ROWS_OUTER ? inner : outer, ROWS_OUTER ? outer : inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
switch (type_) {
|
||||
case IMAGE_TYPE_BINARY: {
|
||||
for (int img_x = img_x0; img_x < w; img_x++) {
|
||||
for (int img_y = img_y0; img_y < h; img_y++) {
|
||||
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);
|
||||
}
|
||||
#ifndef USE_ESP8266
|
||||
// Opaque RGB565 and RGB pixels are already in the layout draw_pixels_at()
|
||||
// takes, so hand the window to the display: drivers with a bulk path blit
|
||||
// it, the others walk it per pixel as before. Not on the ESP8266, where the
|
||||
// pixels live in flash and only progmem_read_byte() may read them.
|
||||
if (this->transparency_ == TRANSPARENCY_OPAQUE &&
|
||||
(this->type_ == IMAGE_TYPE_RGB565 || this->type_ == IMAGE_TYPE_RGB)) {
|
||||
// Clamp to the display so the block never reaches off screen
|
||||
x_start = std::max(x_start, -x);
|
||||
y_start = std::max(y_start, -y);
|
||||
x_end = std::min(x_end, display->get_width() - x);
|
||||
y_end = std::min(y_end, display->get_height() - y);
|
||||
if (x_end <= x_start || y_end <= y_start)
|
||||
return;
|
||||
const bool rgb565 = this->type_ == IMAGE_TYPE_RGB565;
|
||||
display->draw_pixels_at(x + x_start, y + y_start, x_end - x_start, y_end - y_start, this->data_start_,
|
||||
display::COLOR_ORDER_RGB, rgb565 ? display::COLOR_BITNESS_565 : display::COLOR_BITNESS_888,
|
||||
this->big_endian_, x_start, y_start, this->width_ - x_end);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// 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.
|
||||
#ifdef USE_PSRAM
|
||||
if (display->pixel_axes_swapped()) {
|
||||
this->draw_<false>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Without PSRAM the frame buffer is in uncached RAM, where the strided write
|
||||
// is free, so one copy of the loops is enough.
|
||||
this->draw_<true>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
}
|
||||
|
||||
template<bool ROWS_OUTER>
|
||||
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) {
|
||||
// Local copies: draw_pixel_at() is an opaque virtual call, so members read
|
||||
// in the loop would otherwise be reloaded every pixel.
|
||||
const Transparency transparency = this->transparency_;
|
||||
switch (this->type_) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) ESPHOME_ALWAYS_INLINE {
|
||||
if (this->get_binary_pixel_(img_x, img_y)) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_on);
|
||||
} else if (transparency == TRANSPARENCY_OPAQUE) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_off);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case IMAGE_TYPE_GRAYSCALE: {
|
||||
const int width = this->width_;
|
||||
const uint8_t *const data = this->data_start_;
|
||||
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) ESPHOME_ALWAYS_INLINE {
|
||||
const uint8_t gray = progmem_read_byte(data + img_x + img_y * width);
|
||||
Color color = Color(gray, gray, gray, 0xFF);
|
||||
if (transparency == TRANSPARENCY_CHROMA_KEY) {
|
||||
if (gray == 1) {
|
||||
return; // transparent pixel, skip drawing
|
||||
}
|
||||
} else if (transparency == 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);
|
||||
}
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
for (int img_x = img_x0; img_x < w; img_x++) {
|
||||
for (int img_y = img_y0; img_y < h; img_y++) {
|
||||
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;
|
||||
}
|
||||
case IMAGE_TYPE_RGB565:
|
||||
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) ESPHOME_ALWAYS_INLINE {
|
||||
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);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGB565:
|
||||
for (int img_x = img_x0; img_x < w; img_x++) {
|
||||
for (int img_y = img_y0; img_y < h; img_y++) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case IMAGE_TYPE_RGB:
|
||||
for (int img_x = img_x0; img_x < w; img_x++) {
|
||||
for (int img_y = img_y0; img_y < h; img_y++) {
|
||||
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);
|
||||
}
|
||||
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) ESPHOME_ALWAYS_INLINE {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -153,7 +203,7 @@ bool Image::get_binary_pixel_(int x, int y) const {
|
||||
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
|
||||
}
|
||||
Color Image::get_rgb_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
|
||||
const uint32_t pos = (x + y * this->width_) * static_cast<size_t>(this->bpp_) / 8;
|
||||
Color color = Color(progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 1),
|
||||
progmem_read_byte(this->data_start_ + pos + 0), 0xFF);
|
||||
|
||||
@@ -173,8 +223,10 @@ Color Image::get_rgb_pixel_(int x, int y) const {
|
||||
return color;
|
||||
}
|
||||
Color Image::get_rgb565_pixel_(int x, int y) const {
|
||||
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
|
||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos));
|
||||
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * static_cast<size_t>(this->bpp_) / 8;
|
||||
// The high byte sits at pos when big endian, at pos + 1 otherwise
|
||||
const uint8_t high = this->big_endian_ ? 0 : 1;
|
||||
const uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + high), progmem_read_byte(pos + 1 - high));
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
@@ -210,8 +262,14 @@ Color Image::get_grayscale_pixel_(int x, int y) const {
|
||||
int Image::get_width() const { return this->width_; }
|
||||
int Image::get_height() const { return this->height_; }
|
||||
ImageType Image::get_type() const { return this->type_; }
|
||||
Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency)
|
||||
: width_(width), height_(height), type_(type), data_start_(data_start), transparency_(transparency) {
|
||||
Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency,
|
||||
bool big_endian)
|
||||
: width_(width),
|
||||
height_(height),
|
||||
type_(type),
|
||||
data_start_(data_start),
|
||||
transparency_(transparency),
|
||||
big_endian_(big_endian && type == IMAGE_TYPE_RGB565) {
|
||||
switch (this->type_) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
this->bpp_ = 1;
|
||||
|
||||
@@ -23,7 +23,9 @@ enum Transparency {
|
||||
|
||||
class Image : public display::BaseImage {
|
||||
public:
|
||||
Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency);
|
||||
/// big_endian is the RGB565 byte order; codegen writes little endian unless configured otherwise.
|
||||
Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency,
|
||||
bool big_endian = false);
|
||||
Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const;
|
||||
int get_width() const override;
|
||||
int get_height() const override;
|
||||
@@ -43,6 +45,10 @@ class Image : public display::BaseImage {
|
||||
lv_image_dsc_t *get_lv_image_dsc();
|
||||
#endif
|
||||
protected:
|
||||
/// Per-pixel draw of the clipped range, rows (true) or columns (false) outer.
|
||||
template<bool ROWS_OUTER>
|
||||
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;
|
||||
@@ -53,7 +59,8 @@ class Image : public display::BaseImage {
|
||||
ImageType type_;
|
||||
const uint8_t *data_start_;
|
||||
Transparency transparency_;
|
||||
size_t bpp_{};
|
||||
bool big_endian_;
|
||||
uint8_t bpp_{};
|
||||
#ifdef USE_LVGL
|
||||
lv_img_dsc_t dsc_{};
|
||||
#endif
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -206,9 +206,10 @@ void MipiDsi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
if (w <= 0 || h <= 0)
|
||||
return;
|
||||
// if color mapping is required, pass the buck.
|
||||
// note that endianness is not considered here - it is assumed to match!
|
||||
if (bitness != this->color_depth_) {
|
||||
// The buffer holds little endian pixels in the panel's own orientation; anything else goes through
|
||||
// the base class, which converts and rotates one pixel at a time.
|
||||
if (bitness != this->color_depth_ || (bitness == display::COLOR_BITNESS_565 && big_endian) ||
|
||||
this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES) {
|
||||
display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
return;
|
||||
|
||||
@@ -192,6 +192,10 @@ void MipiRgb::update() {
|
||||
} else {
|
||||
this->stop_poller();
|
||||
}
|
||||
this->flush_dirty_();
|
||||
}
|
||||
|
||||
void MipiRgb::flush_dirty_() {
|
||||
if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
|
||||
return;
|
||||
ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_);
|
||||
@@ -210,12 +214,11 @@ void MipiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
if (w <= 0 || h <= 0 || this->is_failed())
|
||||
return;
|
||||
// if color mapping is required, pass the buck.
|
||||
// note that endianness is not considered here - it is assumed to match!
|
||||
if (bitness != display::COLOR_BITNESS_565) {
|
||||
// The panel takes big endian 565 in its own orientation; anything else goes through the base
|
||||
// class, which converts and rotates one pixel at a time into the buffer, then gets flushed.
|
||||
if (bitness != display::COLOR_BITNESS_565 || !big_endian || this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES) {
|
||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<const uint8_t *>(this->buffer_), x_start, y_start,
|
||||
this->width_ - w - x_start);
|
||||
this->flush_dirty_();
|
||||
} else {
|
||||
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ class MipiRgb : public display::Display {
|
||||
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
||||
int x_pad);
|
||||
bool check_buffer_();
|
||||
/// Send the region touched since the last flush to the panel.
|
||||
void flush_dirty_();
|
||||
void dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset);
|
||||
void setup_enables_();
|
||||
void common_setup_();
|
||||
|
||||
@@ -119,19 +119,8 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
int get_width() override {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return HEIGHT;
|
||||
return WIDTH;
|
||||
}
|
||||
|
||||
int get_height() override {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return WIDTH;
|
||||
return HEIGHT;
|
||||
}
|
||||
int get_width() override { return display::rotation_swaps_axes(this->rotation_) ? HEIGHT : WIDTH; }
|
||||
int get_height() override { return display::rotation_swaps_axes(this->rotation_) ? WIDTH : HEIGHT; }
|
||||
|
||||
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
||||
|
||||
@@ -199,9 +188,11 @@ class MipiSpi : public display::Display,
|
||||
return;
|
||||
if (w <= 0 || h <= 0)
|
||||
return;
|
||||
if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN) {
|
||||
// note that the usual logging macros are banned in header files, so use their replacement
|
||||
esph_log_e(TAG, "Unsupported color depth or bit order");
|
||||
if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN ||
|
||||
(!HAS_HARDWARE_ROTATION && this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES)) {
|
||||
// Not the buffer's own format, or rotated in software: go per pixel through draw_pixel_at()
|
||||
display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
return;
|
||||
}
|
||||
this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<const BUFFERTYPE *>(ptr), x_offset, y_offset,
|
||||
@@ -602,6 +593,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))
|
||||
|
||||
@@ -139,7 +139,8 @@ void QspiDbi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8
|
||||
if (w <= 0 || h <= 0)
|
||||
return;
|
||||
if (bitness != display::COLOR_BITNESS_565 || order != this->color_mode_ ||
|
||||
big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
|
||||
big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST) ||
|
||||
this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES) {
|
||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
return;
|
||||
} else if (this->draw_from_origin_) {
|
||||
|
||||
@@ -51,13 +51,18 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
if (w <= 0 || h <= 0)
|
||||
return;
|
||||
// if color mapping is required, pass the buck.
|
||||
// note that endianness is not considered here - it is assumed to match!
|
||||
if (bitness != display::COLOR_BITNESS_565) {
|
||||
// The panel takes big endian 565 in its own orientation; anything else goes through the base
|
||||
// class, which converts and rotates one pixel at a time.
|
||||
if (bitness != display::COLOR_BITNESS_565 || !big_endian || this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES) {
|
||||
display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
return;
|
||||
}
|
||||
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
|
||||
}
|
||||
|
||||
void RpiDpiRgb::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset,
|
||||
int y_offset, int x_pad) {
|
||||
x_start += this->offset_x_;
|
||||
y_start += this->offset_y_;
|
||||
esp_err_t err = ESP_OK;
|
||||
@@ -122,8 +127,7 @@ void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) {
|
||||
}
|
||||
auto pixel = convert_big_endian(display::ColorUtil::color_to_565(color));
|
||||
|
||||
this->draw_pixels_at(x, y, 1, 1, (const uint8_t *) &pixel, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, true,
|
||||
0, 0, 0);
|
||||
this->write_to_display_(x, y, 1, 1, (const uint8_t *) &pixel, 0, 0, 0);
|
||||
this->feed_wdt_per_pixel_();
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ class RpiDpiRgb final : public display::Display {
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
||||
int x_pad);
|
||||
int get_width_internal() override { return this->width_; }
|
||||
int get_height_internal() override { return this->height_; }
|
||||
void reset_display_() const;
|
||||
|
||||
@@ -41,12 +41,11 @@ inline bool is_color_on(const Color &color) {
|
||||
|
||||
RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency,
|
||||
image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height)
|
||||
: Image(nullptr, 0, 0, type, transparency),
|
||||
: Image(nullptr, 0, 0, type, transparency, is_big_endian),
|
||||
format_(format),
|
||||
fixed_width_(fixed_width),
|
||||
fixed_height_(fixed_height),
|
||||
placeholder_(placeholder),
|
||||
is_big_endian_(is_big_endian) {}
|
||||
placeholder_(placeholder) {}
|
||||
|
||||
RuntimeImage::~RuntimeImage() { this->release(); }
|
||||
|
||||
@@ -121,7 +120,7 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
Color mapped_color = color;
|
||||
this->map_chroma_key(mapped_color);
|
||||
uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color);
|
||||
if (this->is_big_endian_) {
|
||||
if (this->big_endian_) {
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>(rgb565 & 0xFF);
|
||||
} else {
|
||||
|
||||
@@ -227,11 +227,6 @@ class RuntimeImage : public image::Image {
|
||||
|
||||
// Configuration
|
||||
bool progressive_display_{false};
|
||||
/**
|
||||
* Whether the image is stored in big-endian format.
|
||||
* This is used to determine how to store 16 bit colors in the buffer.
|
||||
*/
|
||||
bool is_big_endian_{false};
|
||||
/** Whether buffer_ belongs to the caller, so it must not be freed or resized here. */
|
||||
bool external_buffer_{false};
|
||||
};
|
||||
|
||||
@@ -54,13 +54,18 @@ void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
if (w <= 0 || h <= 0)
|
||||
return;
|
||||
// if color mapping is required, pass the buck.
|
||||
// note that endianness is not considered here - it is assumed to match!
|
||||
if (bitness != display::COLOR_BITNESS_565) {
|
||||
// The panel takes big endian 565 in its own orientation; anything else goes through the base
|
||||
// class, which converts and rotates one pixel at a time.
|
||||
if (bitness != display::COLOR_BITNESS_565 || !big_endian || this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES) {
|
||||
display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
return;
|
||||
}
|
||||
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
|
||||
}
|
||||
|
||||
void ST7701S::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
||||
int x_pad) {
|
||||
x_start += this->offset_x_;
|
||||
y_start += this->offset_y_;
|
||||
esp_err_t err;
|
||||
@@ -105,8 +110,7 @@ void ST7701S::draw_pixel_at(int x, int y, Color color) {
|
||||
}
|
||||
auto pixel = convert_big_endian(display::ColorUtil::color_to_565(color));
|
||||
|
||||
this->draw_pixels_at(x, y, 1, 1, (const uint8_t *) &pixel, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, true,
|
||||
0, 0, 0);
|
||||
this->write_to_display_(x, y, 1, 1, (const uint8_t *) &pixel, 0, 0, 0);
|
||||
this->feed_wdt_per_pixel_();
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@ class ST7701S final : public display::Display,
|
||||
|
||||
// this will be horribly slow.
|
||||
protected:
|
||||
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
||||
int x_pad);
|
||||
void write_command_(uint8_t value);
|
||||
void write_data_(uint8_t value);
|
||||
void write_sequence_(uint8_t cmd, size_t len, const uint8_t *bytes);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The column walk in Image::draw() is only built with PSRAM; define it so
|
||||
# the rotated benchmark exercises that loop order on the host.
|
||||
manifest.enable_codegen()
|
||||
original_to_code = manifest.to_code
|
||||
|
||||
async def to_code(config):
|
||||
await original_to_code(config)
|
||||
cg.add_define("USE_PSRAM")
|
||||
|
||||
if hasattr(original_to_code, "priority"):
|
||||
to_code.priority = original_to_code.priority
|
||||
manifest.to_code = to_code
|
||||
@@ -0,0 +1,159 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/image/image.h"
|
||||
|
||||
namespace esphome::image::benchmarks {
|
||||
|
||||
static constexpr int kWidth = 128;
|
||||
static constexpr int kHeight = 96;
|
||||
// Square frame so a rotated draw of the image stays inside it.
|
||||
static constexpr int kStride = kWidth > kHeight ? kWidth : kHeight;
|
||||
|
||||
// One byte per pixel frame buffer with no hardware behind it, so the
|
||||
// measurement is Image::draw() plus the DisplayBuffer pixel path every
|
||||
// buffered display shares. Everything fits the host cache, so this counts
|
||||
// the per pixel work; the memory locality win only shows on hardware.
|
||||
class BenchDisplay : public display::DisplayBuffer {
|
||||
public:
|
||||
BenchDisplay() : frame_(std::make_unique<uint8_t[]>(kStride * kStride)) {}
|
||||
void update() override {}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
int get_width_internal() override { return kStride; }
|
||||
int get_height_internal() override { return kStride; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override { this->frame_[y * kStride + x] = color.r; }
|
||||
|
||||
std::unique_ptr<uint8_t[]> frame_;
|
||||
|
||||
public:
|
||||
uint8_t pixel(int x, int y) const { return this->frame_[y * kStride + x]; }
|
||||
};
|
||||
|
||||
// Models a driver with a bulk path: rows of little endian RGB565 are copied
|
||||
// straight into a 16 bit frame, as the TFT drivers do for their own format.
|
||||
class BulkBenchDisplay : public BenchDisplay {
|
||||
public:
|
||||
BulkBenchDisplay() : frame565_(std::make_unique<uint16_t[]>(kStride * kStride)) {}
|
||||
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 {
|
||||
if (bitness != display::COLOR_BITNESS_565 || big_endian) {
|
||||
BenchDisplay::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
return;
|
||||
}
|
||||
const size_t stride = x_offset + w + x_pad;
|
||||
for (int y = 0; y < h; y++) {
|
||||
memcpy(this->frame565_.get() + (y_start + y) * kStride + x_start, ptr + ((y_offset + y) * stride + x_offset) * 2,
|
||||
w * 2);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
std::unique_ptr<uint16_t[]> frame565_;
|
||||
};
|
||||
|
||||
// Draws the image at rotation 0 and 90 and checks the second frame is the
|
||||
// transpose of the first, which is what a wrong traversal order would break.
|
||||
static bool frames_match(const Image &image) {
|
||||
BenchDisplay plain;
|
||||
BenchDisplay rotated;
|
||||
rotated.set_rotation(display::DISPLAY_ROTATION_90_DEGREES);
|
||||
const_cast<Image &>(image).draw(0, 0, &plain, display::COLOR_ON, display::COLOR_OFF);
|
||||
const_cast<Image &>(image).draw(0, 0, &rotated, display::COLOR_ON, display::COLOR_OFF);
|
||||
for (int y = 0; y < kHeight; y++) {
|
||||
for (int x = 0; x < kWidth; x++) {
|
||||
// DisplayBuffer maps (x, y) to (width - y - 1, x) at 90 degrees
|
||||
if (plain.pixel(x, y) != rotated.pixel(kStride - y - 1, x))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Deterministic pseudo random pixel bytes so the alpha branch sees a mix of
|
||||
// pixels. Sized for the widest format, the image reads only what it needs.
|
||||
static std::unique_ptr<uint8_t[]> make_pixels() {
|
||||
constexpr size_t bytes = kWidth * kHeight * 4;
|
||||
auto data = std::make_unique<uint8_t[]>(bytes);
|
||||
uint32_t seed = 11897;
|
||||
for (size_t i = 0; i < bytes; i++) {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
data[i] = static_cast<uint8_t>(seed >> 16);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
static void draw_image(benchmark::State &state, ImageType type, Transparency transparency,
|
||||
display::DisplayRotation rotation = display::DISPLAY_ROTATION_0_DEGREES) {
|
||||
auto data = make_pixels();
|
||||
Image image(data.get(), kWidth, kHeight, type, transparency);
|
||||
BenchDisplay display;
|
||||
display.set_rotation(rotation);
|
||||
if (!frames_match(image)) {
|
||||
state.SkipWithError("rotated draw is not the transpose of the plain draw");
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto _ : state) {
|
||||
image.draw(0, 0, &display, display::COLOR_ON, display::COLOR_OFF);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kWidth * kHeight);
|
||||
}
|
||||
|
||||
static void ImageDraw_Binary(benchmark::State &state) { draw_image(state, IMAGE_TYPE_BINARY, TRANSPARENCY_OPAQUE); }
|
||||
BENCHMARK(ImageDraw_Binary);
|
||||
|
||||
static void ImageDraw_Grayscale(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_OPAQUE);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Grayscale);
|
||||
|
||||
static void ImageDraw_RGB565(benchmark::State &state) { draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE); }
|
||||
BENCHMARK(ImageDraw_RGB565);
|
||||
|
||||
static void ImageDraw_RGB(benchmark::State &state) { draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_OPAQUE); }
|
||||
BENCHMARK(ImageDraw_RGB);
|
||||
|
||||
// The same opaque RGB565 image on a display with a bulk path.
|
||||
static void ImageDraw_RGB565_Bulk(benchmark::State &state) {
|
||||
auto data = make_pixels();
|
||||
Image image(data.get(), kWidth, kHeight, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE);
|
||||
BulkBenchDisplay display;
|
||||
for (auto _ : state) {
|
||||
image.draw(0, 0, &display, display::COLOR_ON, display::COLOR_OFF);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kWidth * kHeight);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB565_Bulk);
|
||||
|
||||
static void ImageDraw_RGB_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_OPAQUE, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB_Rotated);
|
||||
|
||||
static void ImageDraw_Binary_Transparent(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_BINARY, TRANSPARENCY_CHROMA_KEY);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Binary_Transparent);
|
||||
|
||||
static void ImageDraw_Grayscale_ChromaKey(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_CHROMA_KEY);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Grayscale_ChromaKey);
|
||||
|
||||
static void ImageDraw_Grayscale_Alpha(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_ALPHA_CHANNEL);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Grayscale_Alpha);
|
||||
|
||||
static void ImageDraw_RGB_Alpha(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_ALPHA_CHANNEL);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB_Alpha);
|
||||
|
||||
} // namespace esphome::image::benchmarks
|
||||
@@ -0,0 +1,12 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The column walk in Display::draw_pixels_at() is only built with PSRAM;
|
||||
# define it so the host test can run both walk orders.
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
cg.add_define("USE_PSRAM")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -0,0 +1,82 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
|
||||
namespace esphome::display::testing {
|
||||
|
||||
// Records every pixel write; pixel_axes_swapped() picks the walk order.
|
||||
class RecordingDisplay : public Display {
|
||||
public:
|
||||
void set_axes_swapped(bool swapped) { this->axes_swapped_ = swapped; }
|
||||
bool pixel_axes_swapped() const override { return this->axes_swapped_; }
|
||||
void draw_pixel_at(int x, int y, Color color) override { this->pixels_.emplace_back(x, y, color.raw_32); }
|
||||
void update() override {}
|
||||
DisplayType get_display_type() override { return DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
int get_width_internal() override { return 64; }
|
||||
int get_height_internal() override { return 64; }
|
||||
|
||||
std::vector<std::tuple<int, int, uint32_t>> sorted_pixels() const {
|
||||
auto sorted = this->pixels_;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
return sorted;
|
||||
}
|
||||
size_t count() const { return this->pixels_.size(); }
|
||||
const std::vector<std::tuple<int, int, uint32_t>> &pixels() const { return this->pixels_; }
|
||||
|
||||
protected:
|
||||
bool axes_swapped_{false};
|
||||
std::vector<std::tuple<int, int, uint32_t>> pixels_;
|
||||
};
|
||||
|
||||
// A 7x5 region with a 2 pixel left offset, a 3 line top offset and 4 pixels of
|
||||
// right padding, so the source index arithmetic of both walks is exercised.
|
||||
static constexpr int W = 7, H = 5, X_OFFSET = 2, Y_OFFSET = 3, X_PAD = 4;
|
||||
static constexpr int LINE_STRIDE = X_OFFSET + W + X_PAD;
|
||||
static constexpr int LINES = Y_OFFSET + H;
|
||||
|
||||
static std::vector<uint8_t> make_source(int bytes_per_pixel) {
|
||||
std::vector<uint8_t> source(LINE_STRIDE * LINES * bytes_per_pixel);
|
||||
uint8_t value = 1;
|
||||
for (auto &byte : source)
|
||||
byte = value++;
|
||||
return source;
|
||||
}
|
||||
|
||||
static void expect_walks_match(ColorBitness bitness, int bytes_per_pixel, bool big_endian) {
|
||||
const auto source = make_source(bytes_per_pixel);
|
||||
RecordingDisplay rows;
|
||||
RecordingDisplay columns;
|
||||
columns.set_axes_swapped(true);
|
||||
rows.draw_pixels_at(10, 20, W, H, source.data(), COLOR_ORDER_RGB, bitness, big_endian, X_OFFSET, Y_OFFSET, X_PAD);
|
||||
columns.draw_pixels_at(10, 20, W, H, source.data(), COLOR_ORDER_RGB, bitness, big_endian, X_OFFSET, Y_OFFSET, X_PAD);
|
||||
EXPECT_EQ(rows.count(), static_cast<size_t>(W * H));
|
||||
EXPECT_EQ(columns.count(), static_cast<size_t>(W * H));
|
||||
EXPECT_EQ(rows.sorted_pixels(), columns.sorted_pixels());
|
||||
}
|
||||
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk332) { expect_walks_match(COLOR_BITNESS_332, 1, true); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk565BigEndian) { expect_walks_match(COLOR_BITNESS_565, 2, true); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk565LittleEndian) { expect_walks_match(COLOR_BITNESS_565, 2, false); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk888BigEndian) { expect_walks_match(COLOR_BITNESS_888, 3, true); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk888LittleEndian) { expect_walks_match(COLOR_BITNESS_888, 3, false); }
|
||||
|
||||
// The column walk really is a different order, otherwise the tests above prove nothing.
|
||||
TEST(DrawPixelsAt, ColumnWalkChangesOrder) {
|
||||
const auto source = make_source(1);
|
||||
RecordingDisplay rows;
|
||||
RecordingDisplay columns;
|
||||
columns.set_axes_swapped(true);
|
||||
rows.draw_pixels_at(0, 0, W, H, source.data(), COLOR_ORDER_RGB, COLOR_BITNESS_332, true, X_OFFSET, Y_OFFSET, X_PAD);
|
||||
columns.draw_pixels_at(0, 0, W, H, source.data(), COLOR_ORDER_RGB, COLOR_BITNESS_332, true, X_OFFSET, Y_OFFSET,
|
||||
X_PAD);
|
||||
ASSERT_GT(rows.count(), 1u);
|
||||
ASSERT_GT(columns.count(), 1u);
|
||||
EXPECT_EQ(std::get<0>(rows.pixels()[1]), 1); // the row walk steps x first
|
||||
EXPECT_EQ(std::get<1>(columns.pixels()[1]), 1); // the column walk steps y first
|
||||
}
|
||||
|
||||
} // namespace esphome::display::testing
|
||||
@@ -0,0 +1,166 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/image/image.h"
|
||||
|
||||
namespace esphome::image::testing {
|
||||
|
||||
class TestDisplay : public display::Display {
|
||||
public:
|
||||
void update() override {}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
int get_width_internal() override { return 64; }
|
||||
int get_height_internal() override { return 48; }
|
||||
};
|
||||
|
||||
struct Blit {
|
||||
int x, y, w, h;
|
||||
const uint8_t *ptr;
|
||||
display::ColorBitness bitness;
|
||||
bool big_endian;
|
||||
int x_offset, y_offset, x_pad;
|
||||
};
|
||||
|
||||
// Records the bulk calls and counts the per pixel calls Image::draw() makes.
|
||||
class RecordingDisplay : public TestDisplay {
|
||||
public:
|
||||
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 {
|
||||
this->blits.push_back({x_start, y_start, w, h, ptr, bitness, big_endian, x_offset, y_offset, x_pad});
|
||||
}
|
||||
void draw_pixel_at(int x, int y, Color color) override { this->pixels++; }
|
||||
|
||||
std::vector<Blit> blits;
|
||||
int pixels{0};
|
||||
};
|
||||
|
||||
// Lets the bulk call fall through to the base class and keeps the pixels it produces.
|
||||
class FallbackDisplay : public TestDisplay {
|
||||
public:
|
||||
void draw_pixel_at(int x, int y, Color color) override { this->pixels.push_back({x, y, color.raw_32}); }
|
||||
|
||||
struct Pixel {
|
||||
int x, y;
|
||||
uint32_t raw;
|
||||
};
|
||||
std::vector<Pixel> pixels;
|
||||
};
|
||||
|
||||
static constexpr int W = 8, H = 6;
|
||||
|
||||
static std::vector<uint8_t> make_data(int bytes_per_pixel) {
|
||||
std::vector<uint8_t> data(W * H * bytes_per_pixel);
|
||||
uint8_t value = 1;
|
||||
for (auto &byte : data)
|
||||
byte = value++;
|
||||
return data;
|
||||
}
|
||||
|
||||
// Draws an opaque RGB565 image at (x, y) and returns the single bulk call it made.
|
||||
static Blit draw_one(int x, int y, const std::vector<uint8_t> &data, RecordingDisplay &display,
|
||||
bool big_endian = false) {
|
||||
Image image(data.data(), W, H, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE, big_endian);
|
||||
image.draw(x, y, &display, Color::WHITE, Color::BLACK);
|
||||
EXPECT_EQ(display.blits.size(), 1u);
|
||||
EXPECT_EQ(display.pixels, 0);
|
||||
return display.blits.at(0);
|
||||
}
|
||||
|
||||
static void expect_window(const Blit &blit, int x, int y, int w, int h, int x_offset, int y_offset, int x_pad) {
|
||||
EXPECT_EQ(blit.x, x);
|
||||
EXPECT_EQ(blit.y, y);
|
||||
EXPECT_EQ(blit.w, w);
|
||||
EXPECT_EQ(blit.h, h);
|
||||
EXPECT_EQ(blit.x_offset, x_offset);
|
||||
EXPECT_EQ(blit.y_offset, y_offset);
|
||||
EXPECT_EQ(blit.x_pad, x_pad);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, OpaqueRgb565IsOneBulkCall) {
|
||||
const auto data = make_data(2);
|
||||
RecordingDisplay display;
|
||||
const Blit blit = draw_one(10, 20, data, display);
|
||||
expect_window(blit, 10, 20, W, H, 0, 0, 0);
|
||||
EXPECT_EQ(blit.ptr, data.data());
|
||||
EXPECT_EQ(blit.bitness, display::COLOR_BITNESS_565);
|
||||
EXPECT_FALSE(blit.big_endian);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, BigEndianRgb565IsPassedThrough) {
|
||||
const auto data = make_data(2);
|
||||
RecordingDisplay display;
|
||||
EXPECT_TRUE(draw_one(0, 0, data, display, true).big_endian);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, OpaqueRgbIsOneBulkCall) {
|
||||
const auto data = make_data(3);
|
||||
Image image(data.data(), W, H, IMAGE_TYPE_RGB, TRANSPARENCY_OPAQUE, true); // byte order only applies to 565
|
||||
RecordingDisplay display;
|
||||
image.draw(0, 0, &display, Color::WHITE, Color::BLACK);
|
||||
ASSERT_EQ(display.blits.size(), 1u);
|
||||
EXPECT_EQ(display.blits[0].bitness, display::COLOR_BITNESS_888);
|
||||
EXPECT_FALSE(display.blits[0].big_endian);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, ClippingShrinksTheWindow) {
|
||||
const auto data = make_data(2);
|
||||
RecordingDisplay display;
|
||||
display.start_clipping(12, 22, 15, 24); // right and bottom are exclusive
|
||||
expect_window(draw_one(10, 20, data, display), 12, 22, 3, 2, 2, 2, W - 5);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, PartlyOffTheTopLeftIsClamped) {
|
||||
const auto data = make_data(2);
|
||||
RecordingDisplay display;
|
||||
expect_window(draw_one(-3, -2, data, display), 0, 0, W - 3, H - 2, 3, 2, 0);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, PartlyOffTheBottomRightIsClamped) {
|
||||
const auto data = make_data(2);
|
||||
RecordingDisplay display;
|
||||
expect_window(draw_one(60, 44, data, display), 60, 44, 4, 4, 0, 0, W - 4);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, FullyOffScreenDrawsNothing) {
|
||||
const auto data = make_data(2);
|
||||
Image image(data.data(), W, H, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE);
|
||||
RecordingDisplay display;
|
||||
image.draw(64, 0, &display, Color::WHITE, Color::BLACK);
|
||||
image.draw(0, -H, &display, Color::WHITE, Color::BLACK);
|
||||
EXPECT_TRUE(display.blits.empty());
|
||||
EXPECT_EQ(display.pixels, 0);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, TransparentAndOtherFormatsStayPerPixel) {
|
||||
const auto rgb565 = make_data(2);
|
||||
const auto gray = make_data(1);
|
||||
Image keyed(rgb565.data(), W, H, IMAGE_TYPE_RGB565, TRANSPARENCY_CHROMA_KEY);
|
||||
Image grayscale(gray.data(), W, H, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_OPAQUE);
|
||||
RecordingDisplay display;
|
||||
keyed.draw(0, 0, &display, Color::WHITE, Color::BLACK);
|
||||
grayscale.draw(0, 0, &display, Color::WHITE, Color::BLACK);
|
||||
EXPECT_TRUE(display.blits.empty());
|
||||
EXPECT_GT(display.pixels, 0);
|
||||
}
|
||||
|
||||
// The base class fallback must read the pixels in the byte order the image declares.
|
||||
static void expect_fallback_decodes(bool big_endian) {
|
||||
const auto data = make_data(2);
|
||||
Image image(data.data(), W, H, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE, big_endian);
|
||||
FallbackDisplay display;
|
||||
image.draw(0, 0, &display, Color::WHITE, Color::BLACK);
|
||||
ASSERT_EQ(display.pixels.size(), static_cast<size_t>(W * H));
|
||||
for (const auto &pixel : display.pixels) {
|
||||
const uint32_t rgb565 =
|
||||
display::ColorUtil::read_packed<display::COLOR_BITNESS_565>(data.data(), pixel.x + pixel.y * W, big_endian);
|
||||
const Color expected = display::ColorUtil::to_color(rgb565, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565);
|
||||
EXPECT_EQ(pixel.raw, expected.raw_32) << "at " << pixel.x << "," << pixel.y;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ImageDraw, FallbackDecodesLittleEndian) { expect_fallback_decodes(false); }
|
||||
TEST(ImageDraw, FallbackDecodesBigEndian) { expect_fallback_decodes(true); }
|
||||
|
||||
} // namespace esphome::image::testing
|
||||
Reference in New Issue
Block a user