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_;
 };