diff --git a/tests/benchmarks/components/display/__init__.py b/tests/benchmarks/components/display/__init__.py new file mode 100644 index 00000000000..34eec8f3748 --- /dev/null +++ b/tests/benchmarks/components/display/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride, add_defines_to_code + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # Loops that only exist on boards with PSRAM are gated on this define; + # set it so the host benchmark measures the same code those boards run. + add_defines_to_code(manifest, "USE_PSRAM") diff --git a/tests/benchmarks/components/display/bench_draw_pixels_at.cpp b/tests/benchmarks/components/display/bench_draw_pixels_at.cpp new file mode 100644 index 00000000000..da96dd83b9a --- /dev/null +++ b/tests/benchmarks/components/display/bench_draw_pixels_at.cpp @@ -0,0 +1,82 @@ +#include + +#include +#include + +#include "esphome/components/display/display_buffer.h" + +namespace esphome::display::benchmarks { + +static constexpr int kWidth = 128; +static constexpr int kHeight = 96; +// Square frame so a rotated block stays inside it. +static constexpr int kStride = kWidth > kHeight ? kWidth : kHeight; + +// A buffered display without its own draw_pixels_at(), so every block goes +// through the base class fallback and the DisplayBuffer pixel path. +class BenchDisplay : public DisplayBuffer { + public: + BenchDisplay() : frame_(std::make_unique(kStride * kStride)) {} + void update() override {} + DisplayType get_display_type() override { return 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 frame_; +}; + +// Deterministic pseudo random pixel bytes, sized for the widest format. +static std::unique_ptr make_pixels() { + constexpr size_t bytes = kWidth * kHeight * 3; + auto data = std::make_unique(bytes); + uint32_t seed = 11897; + for (size_t i = 0; i < bytes; i++) { + seed = seed * 1103515245u + 12345u; + data[i] = static_cast(seed >> 16); + } + return data; +} + +// One full block the way LVGL hands a display its rendering. +static void draw_block(benchmark::State &state, ColorBitness bitness, bool big_endian, + DisplayRotation rotation = DISPLAY_ROTATION_0_DEGREES) { + auto data = make_pixels(); + BenchDisplay display; + display.set_rotation(rotation); + for (auto _ : state) { + display.draw_pixels_at(0, 0, kWidth, kHeight, data.get(), COLOR_ORDER_RGB, bitness, big_endian, 0, 0, 0); + } + state.SetItemsProcessed(state.iterations() * kWidth * kHeight); +} + +static void DrawPixelsAt_RGB565_BigEndian(benchmark::State &state) { draw_block(state, COLOR_BITNESS_565, true); } +BENCHMARK(DrawPixelsAt_RGB565_BigEndian); + +static void DrawPixelsAt_RGB565_LittleEndian(benchmark::State &state) { draw_block(state, COLOR_BITNESS_565, false); } +BENCHMARK(DrawPixelsAt_RGB565_LittleEndian); + +static void DrawPixelsAt_RGB888(benchmark::State &state) { draw_block(state, COLOR_BITNESS_888, true); } +BENCHMARK(DrawPixelsAt_RGB888); + +static void DrawPixelsAt_RGB332(benchmark::State &state) { draw_block(state, COLOR_BITNESS_332, true); } +BENCHMARK(DrawPixelsAt_RGB332); + +static void DrawPixelsAt_RGB565_Rotated(benchmark::State &state) { + draw_block(state, COLOR_BITNESS_565, true, DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(DrawPixelsAt_RGB565_Rotated); + +static void DrawPixelsAt_RGB888_Rotated(benchmark::State &state) { + draw_block(state, COLOR_BITNESS_888, true, DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(DrawPixelsAt_RGB888_Rotated); + +static void DrawPixelsAt_RGB332_Rotated(benchmark::State &state) { + draw_block(state, COLOR_BITNESS_332, true, DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(DrawPixelsAt_RGB332_Rotated); + +} // namespace esphome::display::benchmarks diff --git a/tests/benchmarks/components/image/__init__.py b/tests/benchmarks/components/image/__init__.py new file mode 100644 index 00000000000..34eec8f3748 --- /dev/null +++ b/tests/benchmarks/components/image/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride, add_defines_to_code + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # Loops that only exist on boards with PSRAM are gated on this define; + # set it so the host benchmark measures the same code those boards run. + add_defines_to_code(manifest, "USE_PSRAM") diff --git a/tests/benchmarks/components/image/bench_image_draw.cpp b/tests/benchmarks/components/image/bench_image_draw.cpp new file mode 100644 index 00000000000..6bdd0acffbc --- /dev/null +++ b/tests/benchmarks/components/image/bench_image_draw.cpp @@ -0,0 +1,245 @@ +#include + +#include +#include +#include + +#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, not what a frame buffer in PSRAM costs. +class BenchDisplay : public display::DisplayBuffer { + public: + BenchDisplay() : frame_(std::make_unique(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 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. +// Image::draw() only reaches it once it hands opaque images to +// draw_pixels_at(); until then this case matches ImageDraw_RGB565. +class BulkBenchDisplay : public BenchDisplay { + public: + BulkBenchDisplay() : frame565_(std::make_unique(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 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(Image &image) { + BenchDisplay plain; + BenchDisplay rotated; + rotated.set_rotation(display::DISPLAY_ROTATION_90_DEGREES); + image.draw(0, 0, &plain, display::COLOR_ON, display::COLOR_OFF); + 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 make_pixels() { + constexpr size_t bytes = kWidth * kHeight * 4; + auto data = std::make_unique(bytes); + uint32_t seed = 11897; + for (size_t i = 0; i < bytes; i++) { + seed = seed * 1103515245u + 12345u; + data[i] = static_cast(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); + +static void ImageDraw_RGB_ChromaKey(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_CHROMA_KEY); +} +BENCHMARK(ImageDraw_RGB_ChromaKey); + +static void ImageDraw_RGB565_ChromaKey(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_CHROMA_KEY); +} +BENCHMARK(ImageDraw_RGB565_ChromaKey); + +static void ImageDraw_RGB565_Alpha(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_ALPHA_CHANNEL); +} +BENCHMARK(ImageDraw_RGB565_Alpha); + +static void ImageDraw_RGB111(benchmark::State &state) { draw_image(state, IMAGE_TYPE_RGB111, TRANSPARENCY_OPAQUE); } +BENCHMARK(ImageDraw_RGB111); + +static void ImageDraw_RGB111_Alpha(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_RGB111, TRANSPARENCY_ALPHA_CHANNEL); +} +BENCHMARK(ImageDraw_RGB111_Alpha); + +// The same cases with the display rotated 90 degrees, which walks the other loop order. +static void ImageDraw_Binary_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_BINARY, TRANSPARENCY_OPAQUE, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_Binary_Rotated); + +static void ImageDraw_Grayscale_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_OPAQUE, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_Grayscale_Rotated); + +static void ImageDraw_RGB565_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_RGB565_Rotated); + +static void ImageDraw_Binary_Transparent_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_BINARY, TRANSPARENCY_CHROMA_KEY, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_Binary_Transparent_Rotated); + +static void ImageDraw_Grayscale_ChromaKey_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_CHROMA_KEY, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_Grayscale_ChromaKey_Rotated); + +static void ImageDraw_Grayscale_Alpha_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_ALPHA_CHANNEL, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_Grayscale_Alpha_Rotated); + +static void ImageDraw_RGB_Alpha_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_ALPHA_CHANNEL, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_RGB_Alpha_Rotated); + +static void ImageDraw_RGB_ChromaKey_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_CHROMA_KEY, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_RGB_ChromaKey_Rotated); + +static void ImageDraw_RGB565_ChromaKey_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_CHROMA_KEY, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_RGB565_ChromaKey_Rotated); + +static void ImageDraw_RGB565_Alpha_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_ALPHA_CHANNEL, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_RGB565_Alpha_Rotated); + +static void ImageDraw_RGB111_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_RGB111, TRANSPARENCY_OPAQUE, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_RGB111_Rotated); + +static void ImageDraw_RGB111_Alpha_Rotated(benchmark::State &state) { + draw_image(state, IMAGE_TYPE_RGB111, TRANSPARENCY_ALPHA_CHANNEL, display::DISPLAY_ROTATION_90_DEGREES); +} +BENCHMARK(ImageDraw_RGB111_Alpha_Rotated); + +} // namespace esphome::image::benchmarks diff --git a/tests/benchmarks/components/runtime_image/bench_runtime_image_draw.cpp b/tests/benchmarks/components/runtime_image/bench_runtime_image_draw.cpp new file mode 100644 index 00000000000..fd3cf6fb773 --- /dev/null +++ b/tests/benchmarks/components/runtime_image/bench_runtime_image_draw.cpp @@ -0,0 +1,125 @@ +#include + +#include +#include + +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/runtime_image/runtime_image.h" + +namespace esphome::runtime_image::benchmarks { + +static constexpr int kWidth = 128; +static constexpr int kHeight = 96; + +// One byte per pixel frame buffer with no hardware behind it, so the +// measurement is the image draw plus the DisplayBuffer pixel path every +// buffered display shares. +class BenchDisplay : public display::DisplayBuffer { + public: + 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->frame_[y * kWidth + x] = color.r; } + + std::unique_ptr frame_; +}; + +// A runtime image filled the way a decoder fills it, one draw_pixel() per +// pixel, then published as decoded so draw() uses the buffer. +class BenchImage : public RuntimeImage { + public: + BenchImage(image::ImageType type, image::Transparency transparency, bool big_endian) + : RuntimeImage(JPEG, type, transparency, nullptr, big_endian, kWidth, kHeight) {} + + bool fill() { + if (this->resize(kWidth, kHeight) == 0) + return false; + uint32_t seed = 11897; + for (int y = 0; y < kHeight; y++) { + for (int x = 0; x < kWidth; x++) { + seed = seed * 1103515245u + 12345u; + // Every fourth pixel is transparent so the chroma key cases hit both branches + const uint8_t alpha = (x & 3) == 0 ? 0x00 : 0xFF; + this->draw_pixel(x, y, Color(seed >> 24, seed >> 16, seed >> 8, alpha)); + } + } + this->width_ = this->buffer_width_; + this->height_ = this->buffer_height_; + this->data_start_ = this->buffer_; + return true; + } +}; + +// The decoder side: writing every pixel of the image into its buffer. +static void fill_image(benchmark::State &state, image::ImageType type, image::Transparency transparency) { + BenchImage image(type, transparency, false); + for (auto _ : state) { + if (!image.fill()) { + state.SkipWithError("buffer allocation failed"); + return; + } + benchmark::DoNotOptimize(image.get_data_start()); + } + state.SetItemsProcessed(state.iterations() * kWidth * kHeight); +} + +// The display side: drawing the decoded image onto a buffered display. +static void draw_image(benchmark::State &state, image::ImageType type, image::Transparency transparency, + bool big_endian = false) { + BenchImage image(type, transparency, big_endian); + if (!image.fill()) { + state.SkipWithError("buffer allocation failed"); + return; + } + BenchDisplay display; + for (auto _ : state) { + image.draw(0, 0, &display, display::COLOR_ON, display::COLOR_OFF); + } + state.SetItemsProcessed(state.iterations() * kWidth * kHeight); +} + +static void RuntimeImageFill_RGB565(benchmark::State &state) { + fill_image(state, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE); +} +BENCHMARK(RuntimeImageFill_RGB565); + +static void RuntimeImageFill_RGB565_ChromaKey(benchmark::State &state) { + fill_image(state, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_CHROMA_KEY); +} +BENCHMARK(RuntimeImageFill_RGB565_ChromaKey); + +static void RuntimeImageFill_RGB(benchmark::State &state) { + fill_image(state, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE); +} +BENCHMARK(RuntimeImageFill_RGB); + +static void RuntimeImageDraw_RGB565(benchmark::State &state) { + draw_image(state, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE); +} +BENCHMARK(RuntimeImageDraw_RGB565); + +static void RuntimeImageDraw_RGB565_BigEndian(benchmark::State &state) { + draw_image(state, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE, true); +} +BENCHMARK(RuntimeImageDraw_RGB565_BigEndian); + +static void RuntimeImageDraw_RGB565_ChromaKey(benchmark::State &state) { + draw_image(state, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_CHROMA_KEY); +} +BENCHMARK(RuntimeImageDraw_RGB565_ChromaKey); + +static void RuntimeImageDraw_RGB(benchmark::State &state) { + draw_image(state, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE); +} +BENCHMARK(RuntimeImageDraw_RGB); + +static void RuntimeImageDraw_Grayscale(benchmark::State &state) { + draw_image(state, image::IMAGE_TYPE_GRAYSCALE, image::TRANSPARENCY_OPAQUE); +} +BENCHMARK(RuntimeImageDraw_Grayscale); + +} // namespace esphome::runtime_image::benchmarks diff --git a/tests/testing_helpers.py b/tests/testing_helpers.py index 20b76697a19..23bef718694 100644 --- a/tests/testing_helpers.py +++ b/tests/testing_helpers.py @@ -1,6 +1,7 @@ from typing import Any from esphome.loader import ComponentManifest, _replace_component_manifest +from esphome.types import ConfigType class ComponentManifestOverride: @@ -61,3 +62,24 @@ def set_testing_manifest(domain: str, manifest: ComponentManifestOverride) -> No directory provides an ``override_manifest`` function. """ _replace_component_manifest(domain, manifest) + + +def add_defines_to_code(manifest: ComponentManifestOverride, *defines: str) -> None: + """Keep the component's real ``to_code`` and add the given defines after it. + + For host test and benchmark builds of code that is gated on a platform + define, such as loops that only exist on boards with PSRAM. + """ + from esphome import codegen as cg + + manifest.enable_codegen() + original_to_code = manifest.to_code + + async def to_code(config: ConfigType) -> None: + await original_to_code(config) + for define in defines: + cg.add_define(define) + + if hasattr(original_to_code, "priority"): + to_code.priority = original_to_code.priority + manifest.to_code = to_code