pw_display: Scale framebuffer during update
If framebuffer size does not match the display drivers
configured size then use nearest neighbor algorithm to
scale during the update.
Change-Id: I1f03319ce4a579aa090a91b3e68501978fadf29e
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/experimental/+/124952
Commit-Queue: Chris Mumford <cmumford@google.com>
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 1bc3dc1..bfbf794 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -43,6 +43,7 @@
deps = [
":host_tests(//targets/host:host_debug_tests)",
"$dir_pw_color:tests.run(//targets/host:host_debug_tests)",
+ "$dir_pw_display:tests.run(//targets/host:host_debug_tests)",
"$dir_pw_draw:tests.run(//targets/host:host_debug_tests)",
"$dir_pw_framebuffer:tests.run(//targets/host:host_debug_tests)",
diff --git a/applications/app_common_impl/common_arduino.cc b/applications/app_common_impl/common_arduino.cc
index babf6d5..c8d8ec7 100644
--- a/applications/app_common_impl/common_arduino.cc
+++ b/applications/app_common_impl/common_arduino.cc
@@ -44,8 +44,11 @@
pw::spi::Device device;
};
-constexpr int kNumPixels = LCD_WIDTH * LCD_HEIGHT;
-constexpr int kDisplayRowBytes = sizeof(uint16_t) * LCD_WIDTH;
+constexpr int kScaleFactor = 1;
+constexpr int kFramebufferWidth = LCD_WIDTH / kScaleFactor;
+constexpr int kFramebufferHeight = LCD_HEIGHT / kScaleFactor;
+constexpr int kNumPixels = kFramebufferWidth * kFramebufferHeight;
+constexpr int kFramebufferRowBytes = kFramebufferWidth * sizeof(uint16_t);
constexpr pw::spi::Config kSpiConfig8Bit{
.polarity = pw::spi::ClockPolarity::kActiveHigh,
@@ -87,9 +90,11 @@
.spi_device_16_bit = s_spi_16_bit.device,
});
uint16_t s_pixel_data[kNumPixels];
-Display s_display(
- FramebufferRgb565(s_pixel_data, LCD_WIDTH, LCD_HEIGHT, kDisplayRowBytes),
- s_display_driver);
+Display s_display(FramebufferRgb565(s_pixel_data,
+ kFramebufferWidth,
+ kFramebufferHeight,
+ kFramebufferRowBytes),
+ s_display_driver);
SpiValues::SpiValues(pw::spi::Config config,
pw::spi::ChipSelector& selector,
diff --git a/applications/app_common_impl/common_host_imgui.cc b/applications/app_common_impl/common_host_imgui.cc
index f90b57f..5e35085 100644
--- a/applications/app_common_impl/common_host_imgui.cc
+++ b/applications/app_common_impl/common_host_imgui.cc
@@ -21,14 +21,19 @@
namespace {
-constexpr int kNumPixels = LCD_WIDTH * LCD_HEIGHT;
-constexpr int kDisplayRowBytes = sizeof(uint16_t) * LCD_WIDTH;
+constexpr int kDisplayScaleFactor = 1;
+constexpr int kFramebufferWidth = LCD_WIDTH / kDisplayScaleFactor;
+constexpr int kFramebufferHeight = LCD_HEIGHT / kDisplayScaleFactor;
+constexpr int kNumPixels = kFramebufferWidth * kFramebufferHeight;
+constexpr int FramebufferRowBytes = sizeof(uint16_t) * kFramebufferWidth;
uint16_t s_pixel_data[kNumPixels];
pw::display_driver::DisplayDriverImgUI s_display_driver;
-pw::display::DisplayImgUI s_display(
- FramebufferRgb565(s_pixel_data, LCD_WIDTH, LCD_HEIGHT, kDisplayRowBytes),
- s_display_driver);
+pw::display::DisplayImgUI s_display(FramebufferRgb565(s_pixel_data,
+ kFramebufferWidth,
+ kFramebufferHeight,
+ FramebufferRowBytes),
+ s_display_driver);
} // namespace
diff --git a/applications/app_common_impl/common_pico.cc b/applications/app_common_impl/common_pico.cc
index 40f3068..992aad6 100644
--- a/applications/app_common_impl/common_pico.cc
+++ b/applications/app_common_impl/common_pico.cc
@@ -67,8 +67,11 @@
pw::spi::Device device;
};
-constexpr int kNumPixels = LCD_WIDTH * LCD_HEIGHT;
-constexpr int kDisplayRowBytes = sizeof(uint16_t) * LCD_WIDTH;
+constexpr int kDisplayScaleFactor = 1;
+constexpr int kFramebufferWidth = LCD_WIDTH / kDisplayScaleFactor;
+constexpr int kFramebufferHeight = LCD_HEIGHT / kDisplayScaleFactor;
+constexpr int kNumPixels = kFramebufferWidth * kFramebufferHeight;
+constexpr int FramebufferRowBytes = sizeof(uint16_t) * kFramebufferWidth;
constexpr uint32_t kBaudRate = 31'250'000;
constexpr pw::spi::Config kSpiConfig8Bit{
@@ -111,9 +114,11 @@
.spi_device_16_bit = s_spi_16_bit.device,
});
uint16_t pixel_data[kNumPixels];
-Display s_display(
- FramebufferRgb565(pixel_data, LCD_WIDTH, LCD_HEIGHT, kDisplayRowBytes),
- s_display_driver);
+Display s_display(FramebufferRgb565(pixel_data,
+ kFramebufferWidth,
+ kFramebufferHeight,
+ FramebufferRowBytes),
+ s_display_driver);
#if TFT_BL != -1
void SetBacklight(uint16_t brightness) {
diff --git a/pw_display_driver/BUILD.gn b/pw_display_driver/BUILD.gn
index 5c72c5a..3bf3f50 100644
--- a/pw_display_driver/BUILD.gn
+++ b/pw_display_driver/BUILD.gn
@@ -32,6 +32,7 @@
public = [ "public/pw_display_driver/display_driver.h" ]
public_deps = [
"$dir_pw_framebuffer",
+ "$dir_pw_span",
"$dir_pw_status",
]
}
diff --git a/pw_display_driver/public/pw_display_driver/display_driver.h b/pw_display_driver/public/pw_display_driver/display_driver.h
index 7e87964..38075fc 100644
--- a/pw_display_driver/public/pw_display_driver/display_driver.h
+++ b/pw_display_driver/public/pw_display_driver/display_driver.h
@@ -16,6 +16,7 @@
#include <cstddef>
#include "pw_framebuffer/rgb565.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::display_driver {
@@ -32,9 +33,19 @@
virtual Status Init() = 0;
// Send all pixels in the supplied |framebuffer| to the display controller
- // for display.
+ // for the display.
virtual Status Update(
const pw::framebuffer::FramebufferRgb565& framebuffer) = 0;
+
+ // Send a row of pixels to the display. The number of pixels must be <=
+ // display width.
+ virtual Status WriteRow(span<uint16_t> row_pixels,
+ int row_idx,
+ int col_idx) = 0;
+
+ virtual int GetWidth() const = 0;
+
+ virtual int GetHeight() const = 0;
};
} // namespace pw::display_driver
diff --git a/pw_display_driver_ili9341/display_driver.cc b/pw_display_driver_ili9341/display_driver.cc
index 3ac0f1f..d9d9c2e 100644
--- a/pw_display_driver_ili9341/display_driver.cc
+++ b/pw_display_driver_ili9341/display_driver.cc
@@ -14,6 +14,8 @@
#include "pw_display_driver_ili9341/display_driver.h"
+#include <algorithm>
+
#include "pw_digital_io/digital_io.h"
#include "pw_framebuffer/rgb565.h"
#include "pw_spin_delay/delay.h"
@@ -28,6 +30,7 @@
namespace {
+constexpr std::array<std::byte, 0> kEmptyByteArray = {};
constexpr uint16_t ILI9341_MADCTL = 0x36;
constexpr std::byte MADCTL_MY = std::byte{0x80};
constexpr std::byte MADCTL_MX = std::byte{0x40};
@@ -37,6 +40,9 @@
constexpr std::byte MADCTL_BGR = std::byte{0x08};
constexpr std::byte MADCTL_MH = std::byte{0x04};
+constexpr uint8_t ILI9341_CASET = 0x2a; // Column address set.
+constexpr uint8_t ILI9341_PASET = 0x2b; // Page address set.
+constexpr uint8_t ILI9341_RAMWR = 0x2c; // Memory write.
constexpr uint16_t ILI9341_PIXEL_FORMAT_SET = 0x3A;
// The ILI9341 is hard-coded at 320x240;
@@ -44,6 +50,10 @@
constexpr int kDisplayHeight = 240;
constexpr int kDisplayNumPixels = kDisplayWidth * kDisplayHeight;
+constexpr uint8_t HighByte(uint16_t val) { return val >> 8; }
+
+constexpr uint8_t LowByte(uint16_t val) { return val & 0xff; }
+
} // namespace
DisplayDriverILI9341::DisplayDriverILI9341(const Config& config)
@@ -66,9 +76,9 @@
if (!s.ok())
return s;
+ SetMode(Mode::kData);
if (command.command_data.empty())
return OkStatus();
- SetMode(Mode::kData);
return transaction.Write(command.command_data);
}
@@ -310,31 +320,49 @@
return s;
}
-Status DisplayDriverILI9341::UpdatePixelDouble(
- const FramebufferRgb565& frame_buffer) {
- uint16_t temp_row[kDisplayWidth];
+Status DisplayDriverILI9341::WriteRow(span<uint16_t> row_pixels,
+ int row_idx,
+ int col_idx) {
+ {
+ // Let controller know a write is coming.
+ auto transaction = config_.spi_device_8_bit.StartTransaction(
+ ChipSelectBehavior::kPerWriteRead);
+ // Landscape drawing Column Address Set
+ const uint16_t max_col_idx = std::max(
+ kDisplayWidth - 1, col_idx + static_cast<int>(row_pixels.size()));
+ WriteCommand(transaction,
+ {ILI9341_CASET,
+ std::array<std::byte, 4>{
+ std::byte{HighByte(col_idx)},
+ std::byte{LowByte(col_idx)},
+ std::byte{HighByte(max_col_idx)},
+ std::byte{LowByte(max_col_idx)},
+ }});
+
+ // Page Address Set
+ uint16_t max_row_idx = row_idx;
+ WriteCommand(transaction,
+ {ILI9341_PASET,
+ std::array<std::byte, 4>{
+ std::byte{HighByte(row_idx)},
+ std::byte{LowByte(row_idx)},
+ std::byte{HighByte(max_row_idx)},
+ std::byte{LowByte(max_row_idx)},
+ }});
+ PW_TRY(WriteCommand(transaction, {ILI9341_RAMWR, kEmptyByteArray}));
+ }
+
auto transaction = config_.spi_device_16_bit.StartTransaction(
ChipSelectBehavior::kPerTransaction);
- const color_rgb565_t* const fbdata = frame_buffer.GetFramebufferData();
- for (int y = 0; y < frame_buffer.GetHeight(); y++) {
- // Populate this row with each pixel repeated twice
- for (int x = 0; x < frame_buffer.GetWidth(); x++) {
- temp_row[x * 2] = fbdata[y * frame_buffer.GetWidth() + x];
- temp_row[(x * 2) + 1] = fbdata[y * frame_buffer.GetWidth() + x];
- }
- // Send this row to the display twice.
- auto s = transaction.Write(ConstByteSpan(
- reinterpret_cast<const std::byte*>(temp_row), kDisplayWidth));
- if (!s.ok())
- return s;
- s = transaction.Write(ConstByteSpan(
- reinterpret_cast<const std::byte*>(temp_row), kDisplayWidth));
- if (!s.ok())
- return s;
- }
- return OkStatus();
+ return transaction.Write(
+ ConstByteSpan(reinterpret_cast<const std::byte*>(row_pixels.data()),
+ row_pixels.size()));
}
+int DisplayDriverILI9341::GetWidth() const { return kDisplayWidth; }
+
+int DisplayDriverILI9341::GetHeight() const { return kDisplayHeight; }
+
Status DisplayDriverILI9341::Reset() {
if (!config_.reset_gpio)
return Status::Unavailable();
diff --git a/pw_display_driver_ili9341/public/pw_display_driver_ili9341/display_driver.h b/pw_display_driver_ili9341/public/pw_display_driver_ili9341/display_driver.h
index 0575116..6495557 100644
--- a/pw_display_driver_ili9341/public/pw_display_driver_ili9341/display_driver.h
+++ b/pw_display_driver_ili9341/public/pw_display_driver_ili9341/display_driver.h
@@ -43,8 +43,9 @@
// DisplayDriver implementation:
Status Init() override;
Status Update(const pw::framebuffer::FramebufferRgb565& framebuffer);
- Status UpdatePixelDouble(
- const pw::framebuffer::FramebufferRgb565& framebuffer);
+ Status WriteRow(span<uint16_t> row_pixels, int row_idx, int col_idx) override;
+ int GetWidth() const override;
+ int GetHeight() const override;
private:
enum class Mode {
diff --git a/pw_display_driver_imgui/display_driver.cc b/pw_display_driver_imgui/display_driver.cc
index c8c83ee..ec1534e 100644
--- a/pw_display_driver_imgui/display_driver.cc
+++ b/pw_display_driver_imgui/display_driver.cc
@@ -406,6 +406,24 @@
return OkStatus();
}
+Status DisplayDriverImgUI::WriteRow(span<uint16_t> row_pixels,
+ int row_idx,
+ int col_idx) {
+ RecreateLcdTexture();
+
+ for (auto c : row_pixels) {
+ _SetTexturePixel(col_idx++, row_idx, c);
+ }
+
+ // Rendering here is horribly slow - come up with better solution.
+ Render();
+ return OkStatus();
+}
+
+int DisplayDriverImgUI::GetWidth() const { return kDisplayWidth; }
+
+int DisplayDriverImgUI::GetHeight() const { return kDisplayHeight; }
+
bool DisplayDriverImgUI::NewTouchEvent() { return left_mouse_pressed; }
Vec3Int DisplayDriverImgUI::GetTouchPoint() {
diff --git a/pw_display_driver_imgui/public/pw_display_driver_imgui/display_driver.h b/pw_display_driver_imgui/public/pw_display_driver_imgui/display_driver.h
index ddcc428..97d90ba 100644
--- a/pw_display_driver_imgui/public/pw_display_driver_imgui/display_driver.h
+++ b/pw_display_driver_imgui/public/pw_display_driver_imgui/display_driver.h
@@ -28,6 +28,9 @@
// pw::display_driver::DisplayDriver implementation:
Status Init() override;
Status Update(const pw::framebuffer::FramebufferRgb565& framebuffer) override;
+ Status WriteRow(span<uint16_t> row_pixels, int row_idx, int col_idx) override;
+ int GetWidth() const override;
+ int GetHeight() const override;
private:
void RecreateLcdTexture();
diff --git a/pw_display_driver_null/public/pw_display_driver_null/display_driver.h b/pw_display_driver_null/public/pw_display_driver_null/display_driver.h
index bad771c..c63da73 100644
--- a/pw_display_driver_null/public/pw_display_driver_null/display_driver.h
+++ b/pw_display_driver_null/public/pw_display_driver_null/display_driver.h
@@ -28,6 +28,9 @@
pw::Status Update(const pw::framebuffer::FramebufferRgb565&) override {
return pw::OkStatus();
}
+ Status WriteRow(span<uint16_t>, int, int) override { return pw::OkStatus(); }
+ int GetWidth() const override { return 0; };
+ int GetHeight() const override { return 0; };
};
} // namespace pw::display_driver
diff --git a/pw_display_driver_st7789/display_driver.cc b/pw_display_driver_st7789/display_driver.cc
index 68a577d..5deb13a 100644
--- a/pw_display_driver_st7789/display_driver.cc
+++ b/pw_display_driver_st7789/display_driver.cc
@@ -72,6 +72,10 @@
#define ST7789_MADCTL_HORIZ_ORDER 0b00000100
// clang-format on
+constexpr uint8_t HighByte(uint16_t val) { return val >> 8; }
+
+constexpr uint8_t LowByte(uint16_t val) { return val & 0xff; }
+
} // namespace
DisplayDriverST7789::DisplayDriverST7789(const Config& config)
@@ -196,6 +200,46 @@
ConstByteSpan(reinterpret_cast<const byte*>(fb_data), num_pixels));
}
+Status DisplayDriverST7789::WriteRow(span<uint16_t> row_pixels,
+ int row_idx,
+ int col_idx) {
+ {
+ // Let controller know a write is coming.
+ auto transaction = config_.spi_device_8_bit.StartTransaction(
+ ChipSelectBehavior::kPerWriteRead);
+ // Landscape drawing Column Address Set
+ const uint16_t max_col_idx =
+ std::max(config_.screen_width - 1,
+ col_idx + static_cast<int>(row_pixels.size()));
+ WriteCommand(transaction,
+ {ST7789_CASET,
+ std::array<std::byte, 4>{
+ std::byte{HighByte(col_idx)},
+ std::byte{LowByte(col_idx)},
+ std::byte{HighByte(max_col_idx)},
+ std::byte{LowByte(max_col_idx)},
+ }});
+
+ // Page Address Set
+ uint16_t max_row_idx = row_idx;
+ WriteCommand(transaction,
+ {ST7789_RASET,
+ std::array<std::byte, 4>{
+ std::byte{HighByte(row_idx)},
+ std::byte{LowByte(row_idx)},
+ std::byte{HighByte(max_row_idx)},
+ std::byte{LowByte(max_row_idx)},
+ }});
+ PW_TRY(WriteCommand(transaction, {ST7789_RAMWR, kEmptyArray}));
+ }
+
+ auto transaction = config_.spi_device_16_bit.StartTransaction(
+ ChipSelectBehavior::kPerTransaction);
+ return transaction.Write(
+ ConstByteSpan(reinterpret_cast<const std::byte*>(row_pixels.data()),
+ row_pixels.size()));
+}
+
Status DisplayDriverST7789::Reset() {
if (!config_.reset_gpio)
return Status::Unavailable();
diff --git a/pw_display_driver_st7789/public/pw_display_driver_st7789/display_driver.h b/pw_display_driver_st7789/public/pw_display_driver_st7789/display_driver.h
index f5824bb..b3cdd68 100644
--- a/pw_display_driver_st7789/public/pw_display_driver_st7789/display_driver.h
+++ b/pw_display_driver_st7789/public/pw_display_driver_st7789/display_driver.h
@@ -44,7 +44,10 @@
// DisplayDriver implementation:
Status Init() override;
- Status Update(const pw::framebuffer::FramebufferRgb565& framebuffer) override;
+ Status Update(const pw::framebuffer::FramebufferRgb565& framebuffer);
+ Status WriteRow(span<uint16_t> row_pixels, int row_idx, int col_idx) override;
+ int GetWidth() const override { return config_.screen_width; }
+ int GetHeight() const override { return config_.screen_height; }
private:
enum class Mode {
diff --git a/pw_graphics/pw_display/BUILD.gn b/pw_graphics/pw_display/BUILD.gn
index aea1cb9..05e8ecd 100644
--- a/pw_graphics/pw_display/BUILD.gn
+++ b/pw_graphics/pw_display/BUILD.gn
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_unit_test/test.gni")
config("public_includes") {
include_dirs = [ "public" ]
@@ -30,3 +31,12 @@
]
sources = [ "display.cc" ]
}
+
+pw_test("display_test") {
+ deps = [ ":pw_display" ]
+ sources = [ "display_test.cc" ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":display_test" ]
+}
diff --git a/pw_graphics/pw_display/display.cc b/pw_graphics/pw_display/display.cc
index 881306b..22fe620 100644
--- a/pw_graphics/pw_display/display.cc
+++ b/pw_graphics/pw_display/display.cc
@@ -13,6 +13,9 @@
// the License.
#include "pw_display/display.h"
+#include "pw_status/try.h"
+
+using pw::color::color_rgb565_t;
using pw::framebuffer::FramebufferRgb565;
namespace pw::display {
@@ -23,6 +26,55 @@
Display::~Display() = default;
+Status Display::UpdateNearestNeighbor(const FramebufferRgb565& framebuffer) {
+ PW_ASSERT(framebuffer.IsValid());
+ if (!framebuffer.GetWidth() || !framebuffer.GetHeight())
+ return Status::Internal();
+
+ const int fb_last_row_idx = framebuffer.GetHeight() - 1;
+ const int fb_last_col_idx = framebuffer.GetWidth() - 1;
+
+ constexpr int kResizeBufferNumPixels = 80;
+ color_rgb565_t resize_buffer[kResizeBufferNumPixels];
+
+ const color_rgb565_t* fbdata = framebuffer.GetFramebufferData();
+ constexpr int kBytesPerPixel = sizeof(color_rgb565_t);
+ const int num_src_row_pixels = framebuffer.GetRowBytes() / kBytesPerPixel;
+
+ const int num_dst_rows = display_driver_.GetHeight();
+ const int num_dst_cols = display_driver_.GetWidth();
+ for (int dst_row_idx = 0; dst_row_idx < num_dst_rows; dst_row_idx++) {
+ int src_row_idx = dst_row_idx * fb_last_row_idx / (num_dst_rows - 1);
+ PW_ASSERT(src_row_idx >= 0);
+ PW_ASSERT(src_row_idx < framebuffer.GetHeight());
+ int next_buff_idx = 0;
+ int dst_col_write_idx = 0;
+ for (int dst_col_idx = 0; dst_col_idx < num_dst_cols; dst_col_idx++) {
+ int src_col_idx = dst_col_idx * fb_last_col_idx / (num_dst_cols - 1);
+ PW_ASSERT(src_col_idx >= 0);
+ PW_ASSERT(src_col_idx < framebuffer.GetWidth());
+ int src_pixel_idx = src_row_idx * num_src_row_pixels + src_col_idx;
+ resize_buffer[next_buff_idx++] = fbdata[src_pixel_idx];
+ if (next_buff_idx == kResizeBufferNumPixels) {
+ // Buffer is full, flush it.
+ PW_TRY(display_driver_.WriteRow(
+ span(resize_buffer, kResizeBufferNumPixels),
+ dst_row_idx,
+ dst_col_write_idx));
+ next_buff_idx = 0;
+ dst_col_write_idx += kResizeBufferNumPixels;
+ }
+ }
+
+ if (next_buff_idx) {
+ // Pixels in buffer, flush them.
+ PW_TRY(display_driver_.WriteRow(
+ span(resize_buffer, next_buff_idx), dst_row_idx, dst_col_write_idx));
+ }
+ }
+ return OkStatus();
+}
+
FramebufferRgb565 Display::GetFramebuffer() {
return FramebufferRgb565(framebuffer_.GetFramebufferData(),
framebuffer_.GetWidth(),
@@ -33,6 +85,10 @@
Status Display::ReleaseFramebuffer(FramebufferRgb565 framebuffer) {
if (!framebuffer.IsValid())
return Status::InvalidArgument();
+ if (framebuffer.GetWidth() != display_driver_.GetWidth() ||
+ framebuffer.GetHeight() != display_driver_.GetHeight()) {
+ return UpdateNearestNeighbor(framebuffer);
+ }
return display_driver_.Update(framebuffer);
}
diff --git a/pw_graphics/pw_display/display_test.cc b/pw_graphics/pw_display/display_test.cc
new file mode 100644
index 0000000..f598c5b
--- /dev/null
+++ b/pw_graphics/pw_display/display_test.cc
@@ -0,0 +1,260 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_display/display.h"
+
+#include <array>
+#include <utility>
+
+#include "gtest/gtest.h"
+
+using pw::color::color_rgb565_t;
+using pw::display_driver::DisplayDriver;
+using pw::framebuffer::FramebufferRgb565;
+
+namespace pw::display {
+
+namespace {
+
+constexpr size_t kMaxSavedParams = 10;
+
+enum class CallFunc {
+ Unset,
+ Update,
+ WriteRow,
+};
+
+struct CallParams {
+ CallFunc call_func = CallFunc::Unset;
+ struct {
+ color_rgb565_t* fb_data = nullptr;
+ } update;
+ struct {
+ size_t num_pixels = 0;
+ int row_idx = 0;
+ int col_idx = 0;
+ } write_row;
+};
+
+class TestDisplayDriver : public DisplayDriver {
+ public:
+ TestDisplayDriver(int width, int height) : width_(width), height_(height) {}
+ virtual ~TestDisplayDriver() = default;
+
+ Status Init() override { return OkStatus(); }
+
+ Status Update(const FramebufferRgb565& framebuffer) override {
+ if (next_call_param_idx_ < kMaxSavedParams) {
+ call_params_[next_call_param_idx_].call_func = CallFunc::Update;
+ call_params_[next_call_param_idx_].update.fb_data =
+ framebuffer.GetFramebufferData();
+ next_call_param_idx_++;
+ }
+ return OkStatus();
+ }
+
+ Status WriteRow(span<uint16_t> pixel_data,
+ int row_idx,
+ int col_idx) override {
+ if (next_call_param_idx_ < kMaxSavedParams) {
+ call_params_[next_call_param_idx_].call_func = CallFunc::WriteRow;
+ call_params_[next_call_param_idx_].write_row.num_pixels =
+ pixel_data.size();
+ call_params_[next_call_param_idx_].write_row.row_idx = row_idx;
+ call_params_[next_call_param_idx_].write_row.col_idx = col_idx;
+ next_call_param_idx_++;
+ }
+ return OkStatus();
+ }
+
+ int GetWidth() const override { return width_; }
+
+ int GetHeight() const override { return height_; }
+
+ int GetNumCalls() const {
+ int count = 0;
+ for (size_t i = 0;
+ i < kMaxSavedParams && call_params_[i].call_func != CallFunc::Unset;
+ i++) {
+ count++;
+ }
+ return count;
+ }
+
+ const CallParams& GetCall(size_t call_idx) {
+ PW_ASSERT(call_idx <= call_params_.size());
+
+ return call_params_[call_idx];
+ }
+
+ private:
+ size_t next_call_param_idx_ = 0;
+ std::array<CallParams, kMaxSavedParams> call_params_;
+ const int width_;
+ const int height_;
+};
+
+TEST(Display, ReleaseNoResize) {
+ constexpr int kFramebufferWidth = 2;
+ constexpr int kFramebufferHeight = 1;
+ constexpr int kNumPixels = kFramebufferWidth * kFramebufferHeight;
+ constexpr int kFramebufferRowBytes =
+ sizeof(color_rgb565_t) * kFramebufferWidth;
+ color_rgb565_t pixel_data[kNumPixels];
+
+ TestDisplayDriver test_driver(kFramebufferWidth, kFramebufferHeight);
+ Display display(FramebufferRgb565(pixel_data,
+ kFramebufferWidth,
+ kFramebufferHeight,
+ kFramebufferRowBytes),
+ test_driver);
+ FramebufferRgb565 fb = display.GetFramebuffer();
+ EXPECT_TRUE(fb.IsValid());
+ EXPECT_EQ(kFramebufferWidth, fb.GetWidth());
+ EXPECT_EQ(kFramebufferHeight, fb.GetHeight());
+ EXPECT_EQ(0, test_driver.GetNumCalls());
+
+ display.ReleaseFramebuffer(std::move(fb));
+ ASSERT_EQ(1, test_driver.GetNumCalls());
+ auto call = test_driver.GetCall(0);
+ EXPECT_EQ(CallFunc::Update, call.call_func);
+ EXPECT_EQ(pixel_data, call.update.fb_data);
+}
+
+TEST(Display, ReleaseSmallResize) {
+ constexpr int kDisplayWidth = 8;
+ constexpr int kDisplayHeight = 4;
+ constexpr int kFramebufferWidth = 2;
+ constexpr int kFramebufferHeight = 1;
+ constexpr int kNumPixels = kFramebufferWidth * kFramebufferHeight;
+ constexpr int kFramebufferRowBytes =
+ sizeof(color_rgb565_t) * kFramebufferWidth;
+ color_rgb565_t pixel_data[kNumPixels];
+
+ TestDisplayDriver test_driver(kDisplayWidth, kDisplayHeight);
+ Display display(FramebufferRgb565(pixel_data,
+ kFramebufferWidth,
+ kFramebufferHeight,
+ kFramebufferRowBytes),
+ test_driver);
+ FramebufferRgb565 fb = display.GetFramebuffer();
+ EXPECT_TRUE(fb.IsValid());
+ EXPECT_EQ(kFramebufferWidth, fb.GetWidth());
+ EXPECT_EQ(kFramebufferHeight, fb.GetHeight());
+ EXPECT_EQ(0, test_driver.GetNumCalls());
+
+ display.ReleaseFramebuffer(std::move(fb));
+ ASSERT_EQ(4, test_driver.GetNumCalls());
+ auto call = test_driver.GetCall(0);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(8U, call.write_row.num_pixels);
+ EXPECT_EQ(0, call.write_row.row_idx);
+ EXPECT_EQ(0, call.write_row.col_idx);
+
+ call = test_driver.GetCall(1);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(8U, call.write_row.num_pixels);
+ EXPECT_EQ(1, call.write_row.row_idx);
+ EXPECT_EQ(0, call.write_row.col_idx);
+
+ call = test_driver.GetCall(2);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(8U, call.write_row.num_pixels);
+ EXPECT_EQ(2, call.write_row.row_idx);
+ EXPECT_EQ(0, call.write_row.col_idx);
+
+ call = test_driver.GetCall(3);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(8U, call.write_row.num_pixels);
+ EXPECT_EQ(3, call.write_row.row_idx);
+ EXPECT_EQ(0, call.write_row.col_idx);
+}
+
+TEST(Display, ReleaseWideResize) {
+ // Display width > resize buffer (80 px.) will cause two writes per row.
+ constexpr int kDisplayWidth = 90;
+ constexpr int kDisplayHeight = 4;
+ constexpr int kFramebufferWidth = 2;
+ constexpr int kFramebufferHeight = 1;
+ constexpr int kNumPixels = kFramebufferWidth * kFramebufferHeight;
+ constexpr int kFramebufferRowBytes =
+ sizeof(color_rgb565_t) * kFramebufferWidth;
+ color_rgb565_t pixel_data[kNumPixels];
+
+ TestDisplayDriver test_driver(kDisplayWidth, kDisplayHeight);
+ Display display(FramebufferRgb565(pixel_data,
+ kFramebufferWidth,
+ kFramebufferHeight,
+ kFramebufferRowBytes),
+ test_driver);
+ FramebufferRgb565 fb = display.GetFramebuffer();
+ EXPECT_TRUE(fb.IsValid());
+ EXPECT_EQ(kFramebufferWidth, fb.GetWidth());
+ EXPECT_EQ(kFramebufferHeight, fb.GetHeight());
+ EXPECT_EQ(0, test_driver.GetNumCalls());
+
+ display.ReleaseFramebuffer(std::move(fb));
+ ASSERT_EQ(8, test_driver.GetNumCalls());
+ auto call = test_driver.GetCall(0);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(80U, call.write_row.num_pixels);
+ EXPECT_EQ(0, call.write_row.row_idx);
+ EXPECT_EQ(0, call.write_row.col_idx);
+
+ call = test_driver.GetCall(1);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(10U, call.write_row.num_pixels);
+ EXPECT_EQ(0, call.write_row.row_idx);
+ EXPECT_EQ(80, call.write_row.col_idx);
+
+ call = test_driver.GetCall(2);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(80U, call.write_row.num_pixels);
+ EXPECT_EQ(1, call.write_row.row_idx);
+ EXPECT_EQ(0, call.write_row.col_idx);
+
+ call = test_driver.GetCall(3);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(10U, call.write_row.num_pixels);
+ EXPECT_EQ(1, call.write_row.row_idx);
+ EXPECT_EQ(80, call.write_row.col_idx);
+
+ call = test_driver.GetCall(4);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(80U, call.write_row.num_pixels);
+ EXPECT_EQ(2, call.write_row.row_idx);
+ EXPECT_EQ(0, call.write_row.col_idx);
+
+ call = test_driver.GetCall(5);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(10U, call.write_row.num_pixels);
+ EXPECT_EQ(2, call.write_row.row_idx);
+ EXPECT_EQ(80, call.write_row.col_idx);
+
+ call = test_driver.GetCall(6);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(80U, call.write_row.num_pixels);
+ EXPECT_EQ(3, call.write_row.row_idx);
+ EXPECT_EQ(0, call.write_row.col_idx);
+
+ call = test_driver.GetCall(7);
+ EXPECT_EQ(CallFunc::WriteRow, call.call_func);
+ EXPECT_EQ(10U, call.write_row.num_pixels);
+ EXPECT_EQ(3, call.write_row.row_idx);
+ EXPECT_EQ(80, call.write_row.col_idx);
+}
+
+} // namespace
+
+} // namespace pw::display
diff --git a/pw_graphics/pw_display/public/pw_display/display.h b/pw_graphics/pw_display/public/pw_display/display.h
index 1b91aaa..ac1b8c3 100644
--- a/pw_graphics/pw_display/public/pw_display/display.h
+++ b/pw_graphics/pw_display/public/pw_display/display.h
@@ -63,6 +63,11 @@
}
private:
+ // Update screen while scaling the framebuffer using nearest
+ // neighbor algorithm.
+ Status UpdateNearestNeighbor(
+ const pw::framebuffer::FramebufferRgb565& framebuffer);
+
pw::framebuffer::FramebufferRgb565 framebuffer_;
pw::display_driver::DisplayDriver& display_driver_;
};