diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h index 608642caa44..be39e2a3e2f 100644 --- a/esphome/components/display/display_color_utils.h +++ b/esphome/components/display/display_color_utils.h @@ -1,4 +1,6 @@ #pragma once +#include +#include #include "esphome/core/color.h" namespace esphome::display { @@ -154,4 +156,54 @@ class ColorUtil { return color; } }; + +/** Rec.601 luma weights (0.299/0.587/0.114), scaled by 256 */ +static constexpr uint16_t REC601_LUMA_R = 77; +static constexpr uint16_t REC601_LUMA_G = 150; +static constexpr uint16_t REC601_LUMA_B = 29; + +/** Rec.601 luma of an RGB color, for optimum perceptual brightness */ +constexpr uint8_t rec601_luma(Color color) { + return (uint8_t) ((REC601_LUMA_R * color.r + REC601_LUMA_G * color.g + REC601_LUMA_B * color.b + 128u) >> 8); +} + +/** Map an RGB color to a single monochrome bit: 1 = white, 0 = black */ +constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; } + +/** color_to_mono() of an RGB565 value as ColorUtil::to_color() reads it, from table lookups instead of a division + * per channel. + */ +class Rgb565ToMono { + public: + explicit Rgb565ToMono(ColorOrder order) { + // Rec.601 weight of the 5-bit, 6-bit and 5-bit fields, by the channel each one holds in this order + uint16_t high = REC601_LUMA_R, middle = REC601_LUMA_G, low = REC601_LUMA_B; + if (order == COLOR_ORDER_BGR) { + high = REC601_LUMA_B; + low = REC601_LUMA_R; + } else if (order == COLOR_ORDER_GRB) { + high = REC601_LUMA_G; + middle = REC601_LUMA_R; + } + // Each field value scaled to 0..255, then weighted + for (unsigned i = 0; i != 32; i++) { + this->high_[i] = high * esp_scale(i, 31); + this->low_[i] = low * esp_scale(i, 31); + } + for (unsigned i = 0; i != 64; i++) + this->middle_[i] = middle * esp_scale(i, 63); + } + + uint8_t operator()(uint16_t value) const { + const uint32_t weighted = this->high_[value >> 11] + this->middle_[(value >> 5) & 0x3F] + this->low_[value & 0x1F]; + return ((weighted + 128u) >> 8) >= 128 ? 1 : 0; + } + + protected: + // Weighted contribution of each value of each field + std::array high_{}; + std::array middle_{}; + std::array low_{}; +}; + } // namespace esphome::display diff --git a/esphome/components/epaper_spi/colorconv.h b/esphome/components/epaper_spi/colorconv.h index 0240aa5cccf..cb59538abab 100644 --- a/esphome/components/epaper_spi/colorconv.h +++ b/esphome/components/epaper_spi/colorconv.h @@ -2,6 +2,7 @@ #include #include +#include "esphome/components/display/display_color_utils.h" #include "esphome/core/color.h" /* Utility for converting internal \a Color RGB representation to supported IC hardware color keys @@ -13,21 +14,15 @@ namespace esphome::epaper_spi { +// The Rec.601 luma helpers and the RGB565 mono lookup live in the shared display utilities, so other +// drivers (e.g. it8951 in its 1 bit per pixel mode) can use them too. +using display::color_to_mono; +using display::rec601_luma; +using display::Rgb565ToMono; + /** Delta for when to regard as gray */ static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50; -/** Rec.601 luma (0.299/0.587/0.114 weights, scaled by 256) for optimum perceptual brightness */ -constexpr uint8_t rec601_luma(Color color) { - return (uint8_t) ((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8); -} - -/** Map RGB color to a single monochrome bit - * - * @param color RGB color to convert from - * @return 1 = white, 0 = black - */ -constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; } - /** Map RGB color to one of 4 discrete gray levels (2 bits per pixel) * * @param color RGB color to convert from diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index 0c1d9b0fafb..2a2f74763f0 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -345,6 +345,120 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) { } } +namespace { +// One source row of an RGB565 block, written into a contiguous 1 bit per pixel buffer. Without SwapAxes the row lies +// along a buffer row and the bit moves by step; with it the row lies along a bit column and the byte moves by step +// rows. +template +void HOT write_rgb565_row(uint8_t *byte, uint8_t mask, int step, size_t row_width, const uint8_t *source, int count, + const Rgb565ToMono &to_mono) { + const ptrdiff_t byte_step = SwapAxes ? step * static_cast(row_width) : 0; + for (; count != 0; count--, source += 2) { + const uint16_t value = BigEndianSource ? (source[0] << 8) | source[1] : source[0] | (source[1] << 8); + if (to_mono(value)) { + *byte |= mask; + } else { + *byte &= ~mask; + } + if constexpr (SwapAxes) { + byte += byte_step; + } else if (step > 0) { + mask >>= 1; + if (mask == 0) { + mask = 0x80; + byte++; + } + } else { + mask <<= 1; + if (mask == 0) { + mask = 0x01; + byte--; + } + } + } +} +} // namespace + +// Each source column maps to one buffer coordinate and each source row to the other, each moving by 1 per pixel, so +// the visible part of a block and its dirty area are worked out once for the whole block. +void HOT EPaperBase::draw_rgb565_block_(uint8_t *buffer, 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 bool swap = this->effective_transform_ & SWAP_XY; + const bool mirror_x = this->effective_transform_ & MIRROR_X; + const bool mirror_y = this->effective_transform_ & MIRROR_Y; + // The buffer axis a source column moves along, and the one a source row moves along + const int column_limit = swap ? this->height_ : this->width_; + const int row_limit = swap ? this->width_ : this->height_; + const bool column_mirrored = swap ? mirror_y : mirror_x; + const bool row_mirrored = swap ? mirror_x : mirror_y; + const int first_column = std::max(0, -x_start); + const int end_column = std::min(w, column_limit - x_start); + const int first_row = std::max(0, -y_start); + const int end_row = std::min(h, row_limit - y_start); + if (first_column >= end_column || first_row >= end_row) + return; + auto column_to_buffer = [&](int column) { + const int u = x_start + column; + return column_mirrored ? column_limit - 1 - u : u; + }; + auto row_to_buffer = [&](int row) { + const int v = y_start + row; + return row_mirrored ? row_limit - 1 - v : v; + }; + + const int column_low = std::min(column_to_buffer(first_column), column_to_buffer(end_column - 1)); + const int row_low = std::min(row_to_buffer(first_row), row_to_buffer(end_row - 1)); + const int columns = end_column - first_column; + const int rows = end_row - first_row; + const int x_low = swap ? row_low : column_low; + const int y_low = swap ? column_low : row_low; + const int x_high = x_low + (swap ? rows : columns); + const int y_high = y_low + (swap ? columns : rows); + + const Rgb565ToMono to_mono(order); + const size_t line_stride = x_offset + w + x_pad; + const size_t row_width = this->row_width_; + const int step = column_mirrored ? -1 : 1; + for (int row = first_row; row != end_row; row++) { + const uint8_t *source = ptr + ((y_offset + row) * line_stride + x_offset + first_column) * 2; + const int along = column_to_buffer(first_column); + const int across = row_to_buffer(row); + const int x = swap ? across : along; + const int y = swap ? along : across; + uint8_t *byte = buffer + y * row_width + x / 8; + const uint8_t mask = 0x80u >> (x % 8); + if (swap) { + if (big_endian) { + write_rgb565_row(byte, mask, step, row_width, source, columns, to_mono); + } else { + write_rgb565_row(byte, mask, step, row_width, source, columns, to_mono); + } + } else if (big_endian) { + write_rgb565_row(byte, mask, step, row_width, source, columns, to_mono); + } else { + write_rgb565_row(byte, mask, step, row_width, source, columns, to_mono); + } + } + this->x_low_ = clamp_at_most(this->x_low_, x_low); + this->x_high_ = clamp_at_least(this->x_high_, x_high); + this->y_low_ = clamp_at_most(this->y_low_, y_low); + this->y_high_ = clamp_at_least(this->y_high_, y_high); +} + +// Writes an RGB565 block of pixels, as LVGL sends them, straight into a 1 bit per pixel buffer that is one block (the +// usual case): the same result as draw_pixel_at() for each pixel. Anything else goes through draw_pixel_at(). +void HOT EPaperBase::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 contiguous_length; + uint8_t *contiguous = this->buffer_.get_span(0, contiguous_length); + if (!this->buffer_is_mono() || this->get_clipping().is_set() || bitness != COLOR_BITNESS_565 || + contiguous_length != this->buffer_length_) { + Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + return; + } + this->draw_rgb565_block_(contiguous, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset, x_pad); +} + void EPaperBase::dump_config() { LOG_DISPLAY("", "E-Paper SPI", this); ESP_LOGCONFIG(TAG, diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index 96755a404ec..c7ef90f1160 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -107,6 +107,8 @@ 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_; } void draw_pixel_at(int x, int y, Color color) override; + 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) override; // Make the next update a full one. Applied when that update starts, so one in progress is not affected. void request_full_update() { this->full_update_requested_ = true; } // True from the start of an update until the display has refreshed and gone back to sleep. @@ -128,6 +130,10 @@ class EPaperBase : public Display, bool init_buffer_(size_t buffer_length); void update_effective_transform_(); bool rotate_coordinates_(int &x, int &y); + void draw_rgb565_block_(uint8_t *buffer, 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); + // Whether the buffer holds 1 bit per pixel as draw_pixel_at() writes it, so draw_pixels_at() can write it directly + virtual bool buffer_is_mono() const { return false; } /** * Methods that must be implemented by concrete classes to control the display diff --git a/esphome/components/epaper_spi/epaper_spi_mono.h b/esphome/components/epaper_spi/epaper_spi_mono.h index d0740595fc5..0f570149de4 100644 --- a/esphome/components/epaper_spi/epaper_spi_mono.h +++ b/esphome/components/epaper_spi/epaper_spi_mono.h @@ -15,6 +15,7 @@ class EPaperMono : public EPaperBase { } protected: + bool buffer_is_mono() const override { return true; } void refresh_screen(bool partial) override; void power_on() override {} void power_off() override{}; diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp b/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp index a9245fa1c2b..a79bbc3dc91 100644 --- a/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp +++ b/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp @@ -58,7 +58,9 @@ bool HOT EPaperSSD1677::transfer_data() { // A full update ignores 0x26, and the copy may not hold a real frame yet (first update after // boot): send the new frame to both planes. const bool send_copy = this->plane_ == 0 && this->update_count_ != 0; - const bool direct = send_copy || this->buffer_is_plane(); + // A mono buffer already holds the frame as a RAM plane does, so it can be sent as it is; a deeper + // buffer (the four-level model) returns false and packs the plane in plane_row(). + const bool direct = send_copy || this->buffer_is_mono(); const auto &source = send_copy ? this->sent_ : this->buffer_; const size_t row_length = this->plane_row_length_(); const size_t plane_length = row_length * this->height_; diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1677.h b/esphome/components/epaper_spi/epaper_spi_ssd1677.h index ddf3d4e0892..b8c93a1ecb8 100644 --- a/esphome/components/epaper_spi/epaper_spi_ssd1677.h +++ b/esphome/components/epaper_spi/epaper_spi_ssd1677.h @@ -34,9 +34,6 @@ class EPaperSSD1677 : public EPaperMono { void init_comparison_frame_(); // Bytes in one row of a RAM plane: 8 pixels per byte, whatever the buffer holds. size_t plane_row_length_() const { return (this->width_ + 7) / 8; } - // Whether the buffer already holds the frame as a RAM plane does, so it can be sent as it is. - // A subclass with a deeper buffer returns false and overrides plane_row(). - virtual bool buffer_is_plane() const { return true; } // Row y of the frame as a RAM plane holds it, 1 bit per pixel with 1 = white. virtual void plane_row(size_t y, uint8_t *out); bool reset() override; diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1677_gray4.h b/esphome/components/epaper_spi/epaper_spi_ssd1677_gray4.h index 47436d2db1d..f21ab3eb069 100644 --- a/esphome/components/epaper_spi/epaper_spi_ssd1677_gray4.h +++ b/esphome/components/epaper_spi/epaper_spi_ssd1677_gray4.h @@ -38,9 +38,9 @@ class EPaperSSD1677Gray4 : public EPaperSSD1677 { void draw_pixel_at(int x, int y, Color color) override; protected: + bool buffer_is_mono() const override { return false; } // A partial update if partial updates are enabled and this is not a full one. bool is_partial_push_() const { return this->update_count_ != 0 && this->sent_.is_valid(); } - bool buffer_is_plane() const override { return false; } void plane_row(size_t y, uint8_t *out) override; void refresh_screen(bool partial) override; bool transfer_data() override; diff --git a/esphome/components/epaper_spi/epaper_spi_uc8179.h b/esphome/components/epaper_spi/epaper_spi_uc8179.h index 85c0eb623ee..b12ed54ef6d 100644 --- a/esphome/components/epaper_spi/epaper_spi_uc8179.h +++ b/esphome/components/epaper_spi/epaper_spi_uc8179.h @@ -37,6 +37,7 @@ class EPaperUC8179 final : public EPaperBase { } protected: + bool buffer_is_mono() const override { return true; } bool initialise(bool partial) override; bool transfer_data() override; void refresh_screen(bool partial) override; diff --git a/esphome/components/it8951/it8951.cpp b/esphome/components/it8951/it8951.cpp index fe99017774d..565f6f7f8bb 100644 --- a/esphome/components/it8951/it8951.cpp +++ b/esphome/components/it8951/it8951.cpp @@ -891,12 +891,10 @@ static uint8_t color_to_nibble(const Color &color) { if (color.raw_32 == 0xFFFFFFFF) return 0x0F; // white - // Derive luma from RGB using Rec.601 weights (0.299/0.587/0.114, scaled by - // 256). Rec.601 is the standard for converting SDR images to grayscale and - // spreads saturated colours across the mid-range; Rec.709 instead crams them - // against white/black where the 16 panel levels are hard to tell apart. - auto luma = static_cast((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8); - return quantize_8bit_to_nibble(luma); + // Rec.601 luma is the standard for converting SDR images to grayscale and spreads saturated colours + // across the mid-range; Rec.709 instead crams them against white/black where the 16 panel levels are + // hard to tell apart. + return quantize_8bit_to_nibble(display::rec601_luma(color)); } // 4x4 ordered (Bayer) dither threshold over the weighted-luma range (0..65535). diff --git a/tests/components/epaper_spi/display/test_draw_pixels.cpp b/tests/components/epaper_spi/display/test_draw_pixels.cpp new file mode 100644 index 00000000000..a3b47ea8fc1 --- /dev/null +++ b/tests/components/epaper_spi/display/test_draw_pixels.cpp @@ -0,0 +1,193 @@ +#include + +#include +#include + +#include "../common.h" +#include "esphome/components/epaper_spi/colorconv.h" +#include "esphome/components/epaper_spi/epaper_spi_ssd1677_gray4.h" +#include "esphome/components/epaper_spi/epaper_spi_ssd1683.h" + +namespace esphome::epaper_spi::testing { + +/// Exposes the buffer and the dirty bounds so the fast path can be compared with drawing pixel by pixel. +template class TestableDisplay : public Driver { + public: + TestableDisplay(uint16_t width, uint16_t height) : Driver("test", width, height, nullptr, 0) { + EXPECT_TRUE(this->init_buffer_(this->buffer_length_)); + this->reset_bounds(); + } + + void reset_bounds() { + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; + } + + std::vector buffer() const { + std::vector out; + for (size_t i = 0; i != this->buffer_length_; i++) + out.push_back(this->buffer_[i]); + return out; + } + + /// Reallocates the buffer in blocks of at most max_block bytes, as SplitBuffer does when memory is fragmented. + void split_buffer(size_t max_block) { + ASSERT_TRUE(this->buffer_.init(this->buffer_length_, max_block)); + ASSERT_GT(this->buffer_.get_buffer_count(), 1u); + this->clear(); + this->reset_bounds(); + } + + std::vector bounds() const { return {this->x_low_, this->y_low_, this->x_high_, this->y_high_}; } + + void draw_one_by_one(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) { + // The per-pixel path the fast one must match + display::Display::draw_pixels_at( // NOLINT(bugprone-parent-virtual-call) + x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + } +}; + +/// RGB565 data with a spread of light and dark pixels, including values either side of the threshold. +static std::vector rgb565(size_t pixels, uint32_t seed) { + std::vector out; + uint32_t state = seed; + for (size_t i = 0; i != pixels; i++) { + state = state * 1103515245u + 12345u; + const uint16_t pixel = static_cast(state >> 8); + out.push_back(pixel & 0xFF); + out.push_back(pixel >> 8); + } + return out; +} + +/// Random bytes for a block of pixels, enough for 3 bytes a pixel. +static std::vector random_bytes(size_t pixels, uint32_t seed) { + std::vector out; + uint32_t state = seed; + for (size_t i = 0; i != pixels * 3; i++) { + state = state * 1103515245u + 12345u; + out.push_back(static_cast(state >> 16)); + } + return out; +} + +/// For every rotation, mirroring and byte order, with source offsets and padding, and with blocks inside the panel, +/// running off each edge and larger than the panel, drawing an RGB565 block directly gives the same buffer and the same +/// dirty area as drawing it pixel by pixel. The panel width is not a multiple of 8. +TEST(EPaperDrawPixels, SameResultAsDrawingPixelByPixel) { + struct Block { + int x, y, w, h; + }; + const Block blocks[] = {{6, 5, 12, 6}, {-3, -2, 10, 8}, {14, 8, 12, 9}, {-5, 4, 30, 3}, {-2, -3, 26, 20}}; + const display::DisplayRotation rotations[] = { + display::DISPLAY_ROTATION_0_DEGREES, display::DISPLAY_ROTATION_90_DEGREES, display::DISPLAY_ROTATION_180_DEGREES, + display::DISPLAY_ROTATION_270_DEGREES}; + // Swapping the axes comes from the 90 and 270 degree rotations + const uint8_t mirrorings[] = {NONE, MIRROR_X, MIRROR_Y, MIRROR_X | MIRROR_Y}; + uint32_t seed = 1; + for (const auto rotation : rotations) { + for (const uint8_t mirroring : mirrorings) { + for (const bool big_endian : {false, true}) { + for (const auto &block : blocks) { + TestableDisplay fast(21, 13); + TestableDisplay slow(21, 13); + for (auto *display : {&fast, &slow}) { + display->set_transform(mirroring); + display->set_rotation(rotation); + } + // a 3 pixel left offset, 2 rows skipped and 4 pixels of padding + const auto data = random_bytes((3 + block.w + 4) * (2 + block.h), seed++); + fast.draw_pixels_at(block.x, block.y, block.w, block.h, data.data(), display::COLOR_ORDER_RGB, + display::COLOR_BITNESS_565, big_endian, 3, 2, 4); + slow.draw_one_by_one(block.x, block.y, block.w, block.h, data.data(), display::COLOR_ORDER_RGB, + display::COLOR_BITNESS_565, big_endian, 3, 2, 4); + const auto where = ::testing::Message() << "rotation " << rotation << " mirroring " << int(mirroring) + << " big endian " << big_endian << " block " << block.x << "," + << block.y << " " << block.w << "x" << block.h; + EXPECT_EQ(fast.buffer(), slow.buffer()) << where; + EXPECT_EQ(fast.bounds(), slow.bounds()) << where; + } + } + } + } +} + +/// The colour order reaches the RGB565 block writer. +TEST(EPaperDrawPixels, ColourOrderIsHonoured) { + for (const auto order : {display::COLOR_ORDER_RGB, display::COLOR_ORDER_BGR, display::COLOR_ORDER_GRB}) { + TestableDisplay fast(21, 13); + TestableDisplay slow(21, 13); + const auto data = random_bytes(21 * 13, 99); + fast.draw_pixels_at(0, 0, 21, 13, data.data(), order, display::COLOR_BITNESS_565, false, 0, 0, 0); + slow.draw_one_by_one(0, 0, 21, 13, data.data(), order, display::COLOR_BITNESS_565, false, 0, 0, 0); + EXPECT_EQ(fast.buffer(), slow.buffer()) << "order " << int(order); + } +} + +/// With a clipping region set the block goes through draw_pixel_at(), which honours it. +TEST(EPaperDrawPixels, ClippingFallsBackToDrawingPixelByPixel) { + TestableDisplay fast(24, 16); + TestableDisplay slow(24, 16); + const auto data = rgb565(24 * 16, 7); + for (auto *display : {&fast, &slow}) + display->start_clipping(4, 4, 12, 10); + fast.draw_pixels_at(0, 0, 24, 16, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0, 0); + slow.draw_one_by_one(0, 0, 24, 16, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0, 0); + EXPECT_EQ(fast.buffer(), slow.buffer()); + EXPECT_EQ(fast.bounds(), (std::vector{4, 4, 12, 10})); +} + +/// A driver whose buffer is not 1 bit per pixel keeps its own draw_pixel_at() for blocks too. +TEST(EPaperDrawPixels, OtherBufferLayoutsDrawPixelByPixel) { + TestableDisplay fast(16, 4); + TestableDisplay slow(16, 4); + const auto data = rgb565(16 * 4, 11); + fast.draw_pixels_at(0, 0, 16, 4, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0, 0); + slow.draw_one_by_one(0, 0, 16, 4, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0, 0); + EXPECT_EQ(fast.buffer(), slow.buffer()); +} + +/// The RGB565 lookup gives exactly what converting the colour and thresholding it does, for every value and order. +TEST(EPaperDrawPixels, Rgb565LookupMatchesTheColourConversion) { + for (const auto order : {display::COLOR_ORDER_RGB, display::COLOR_ORDER_BGR, display::COLOR_ORDER_GRB}) { + const Rgb565ToMono to_mono(order); + size_t mismatches = 0; + for (uint32_t value = 0; value != 0x10000; value++) { + const uint8_t expected = color_to_mono(ColorUtil::to_color(value, order, display::COLOR_BITNESS_565)); + if (to_mono(static_cast(value)) != expected) + mismatches++; + } + EXPECT_EQ(mismatches, 0u) << "order " << int(order); + } +} + +/// Other bit depths, and a buffer split into several blocks, go through draw_pixel_at() and give the same result. +TEST(EPaperDrawPixels, OtherCasesDrawPixelByPixel) { + { + TestableDisplay fast(24, 16); + TestableDisplay slow(24, 16); + fast.split_buffer(7); + slow.split_buffer(7); + const auto data = rgb565(24 * 16, 5); + fast.draw_pixels_at(-2, 3, 24, 16, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0, + 0); + slow.draw_one_by_one(-2, 3, 24, 16, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0, + 0); + EXPECT_EQ(fast.buffer(), slow.buffer()) << "split buffer"; + EXPECT_EQ(fast.bounds(), slow.bounds()) << "split buffer"; + } + for (const auto bitness : {display::COLOR_BITNESS_888, display::COLOR_BITNESS_332}) { + TestableDisplay fast(24, 16); + TestableDisplay slow(24, 16); + const auto data = random_bytes(24 * 16, 9); + fast.draw_pixels_at(0, 0, 24, 16, data.data(), display::COLOR_ORDER_RGB, bitness, false, 0, 0, 0); + slow.draw_one_by_one(0, 0, 24, 16, data.data(), display::COLOR_ORDER_RGB, bitness, false, 0, 0, 0); + EXPECT_EQ(fast.buffer(), slow.buffer()) << "bitness " << int(bitness); + EXPECT_EQ(fast.bounds(), slow.bounds()) << "bitness " << int(bitness); + } +} + +} // namespace esphome::epaper_spi::testing