pw_graphics: Small module refactors

Add bazel and cmake build files for many modules.
Rename pw_math to pw_geometry.
Small updates to formatting and style.

Change-Id: Icbc27a67f9b971c533907c80f9b7d13f186a8e16
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/experimental/+/202871
Reviewed-by: Taylor Cramer <cramertj@google.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/.gn b/.gn
index d231c04..f20251b 100644
--- a/.gn
+++ b/.gn
@@ -22,9 +22,6 @@
   # Sets the FreeRTOS source directory.
   dir_pw_third_party_freertos = "//third_party/freertos/Source"
 
-  # Use the new Python build and merged 'pigweed' Python package.
-  pw_build_USE_NEW_PYTHON_BUILD = true
-
   pw_build_PIP_CONSTRAINTS = [
     # Pigweed upstream constraints
     "$dir_pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list",
diff --git a/applications/32blit_demo/main.cc b/applications/32blit_demo/main.cc
index e2edc05..c7478a1 100644
--- a/applications/32blit_demo/main.cc
+++ b/applications/32blit_demo/main.cc
@@ -38,7 +38,7 @@
 #endif  // if defined(USE_FREERTOS)
 
 using pw::color::color_rgb565_t;
-using pw::color::colors_pico8_rgb565;
+using pw::color::kColorsPico8Rgb565;
 using pw::display::Display;
 using pw::framebuffer::Framebuffer;
 using pw::ring_buffer::PrefixedEntryRingBuffer;
diff --git a/applications/app_common_impl/common_arduino.cc b/applications/app_common_impl/common_arduino.cc
index 717733f..5895349 100644
--- a/applications/app_common_impl/common_arduino.cc
+++ b/applications/app_common_impl/common_arduino.cc
@@ -51,8 +51,8 @@
     FRAMEBUFFER_WIDTH >= 0 ? FRAMEBUFFER_WIDTH : DISPLAY_WIDTH;
 constexpr uint16_t kFramebufferHeight = DISPLAY_HEIGHT;
 
-constexpr pw::math::Size<uint16_t> kDisplaySize = {DISPLAY_WIDTH,
-                                                   DISPLAY_HEIGHT};
+constexpr pw::geometry::Size<uint16_t> kDisplaySize = {DISPLAY_WIDTH,
+                                                       DISPLAY_HEIGHT};
 constexpr size_t kNumPixels = kFramebufferWidth * kFramebufferHeight;
 constexpr uint16_t kDisplayRowBytes = sizeof(uint16_t) * kFramebufferWidth;
 
diff --git a/applications/app_common_impl/common_host_imgui.cc b/applications/app_common_impl/common_host_imgui.cc
index bdc099a..0c4ed1a 100644
--- a/applications/app_common_impl/common_host_imgui.cc
+++ b/applications/app_common_impl/common_host_imgui.cc
@@ -26,7 +26,7 @@
 namespace {
 
 constexpr uint16_t kDisplayScaleFactor = 1;
-constexpr pw::math::Size<uint16_t> kFramebufferDimensions = {
+constexpr pw::geometry::Size<uint16_t> kFramebufferDimensions = {
     .width = DISPLAY_WIDTH / kDisplayScaleFactor,
     .height = DISPLAY_HEIGHT / kDisplayScaleFactor,
 };
@@ -34,8 +34,8 @@
     kFramebufferDimensions.width * kFramebufferDimensions.height;
 constexpr uint16_t kFramebufferRowBytes =
     sizeof(color_rgb565_t) * kFramebufferDimensions.width;
-constexpr pw::math::Size<uint16_t> kDisplaySize = {DISPLAY_WIDTH,
-                                                   DISPLAY_HEIGHT};
+constexpr pw::geometry::Size<uint16_t> kDisplaySize = {DISPLAY_WIDTH,
+                                                       DISPLAY_HEIGHT};
 
 color_rgb565_t s_pixel_data[kNumPixels];
 const pw::Vector<void*, 1> s_pixel_buffers{s_pixel_data};
diff --git a/applications/app_common_impl/common_host_null.cc b/applications/app_common_impl/common_host_null.cc
index 89461a3..70c0f0e 100644
--- a/applications/app_common_impl/common_host_null.cc
+++ b/applications/app_common_impl/common_host_null.cc
@@ -22,8 +22,8 @@
 
 namespace {
 
-constexpr pw::math::Size<uint16_t> kDisplaySize = {DISPLAY_WIDTH,
-                                                   DISPLAY_HEIGHT};
+constexpr pw::geometry::Size<uint16_t> kDisplaySize = {DISPLAY_WIDTH,
+                                                       DISPLAY_HEIGHT};
 
 pw::display_driver::DisplayDriverNULL s_display_driver;
 const pw::Vector<void*, 0> s_pixel_buffers;
diff --git a/applications/app_common_impl/common_mimxrt595.cc b/applications/app_common_impl/common_mimxrt595.cc
index 8da87b9..b8fbb5c 100644
--- a/applications/app_common_impl/common_mimxrt595.cc
+++ b/applications/app_common_impl/common_mimxrt595.cc
@@ -37,14 +37,14 @@
 constexpr uint32_t kBuffer0Addr = 0x28000000U;
 constexpr uint32_t kBuffer1Addr = 0x28200000U;
 constexpr video_pixel_format_t kVideoPixelFormat = kVIDEO_PixelFormatRGB565;
-constexpr pw::math::Size<uint16_t> kFramebufferDimensions = {
+constexpr pw::geometry::Size<uint16_t> kFramebufferDimensions = {
     .width = FRAMEBUFFER_WIDTH >= 0 ? FRAMEBUFFER_WIDTH : DISPLAY_WIDTH,
     .height = DISPLAY_HEIGHT,
 };
 constexpr uint16_t kBufferStrideBytes =
     kFramebufferDimensions.width * pw::mipi::dsi::kBytesPerPixel;
-constexpr pw::math::Size<uint16_t> kDisplaySize = {DISPLAY_WIDTH,
-                                                   DISPLAY_HEIGHT};
+constexpr pw::geometry::Size<uint16_t> kDisplaySize = {DISPLAY_WIDTH,
+                                                       DISPLAY_HEIGHT};
 const pw::Vector<void*, 2> s_framebuffer_addrs = {
     reinterpret_cast<void*>(kBuffer0Addr),
     reinterpret_cast<void*>(kBuffer1Addr)};
diff --git a/applications/app_common_impl/common_pico.cc b/applications/app_common_impl/common_pico.cc
index f831fba..ed37b8a 100644
--- a/applications/app_common_impl/common_pico.cc
+++ b/applications/app_common_impl/common_pico.cc
@@ -90,7 +90,8 @@
                            : DISPLAY_WIDTH / kDisplayScaleFactor;
 constexpr uint16_t kFramebufferHeight = DISPLAY_HEIGHT / kDisplayScaleFactor;
 
-constexpr pw::math::Size<uint16_t> kDisplaySize{DISPLAY_WIDTH, DISPLAY_HEIGHT};
+constexpr pw::geometry::Size<uint16_t> kDisplaySize{DISPLAY_WIDTH,
+                                                    DISPLAY_HEIGHT};
 constexpr size_t kNumPixels = kFramebufferWidth * kFramebufferHeight;
 constexpr uint16_t kFramebufferRowBytes = sizeof(uint16_t) * kFramebufferWidth;
 
diff --git a/applications/app_common_impl/common_stm32cube.cc b/applications/app_common_impl/common_stm32cube.cc
index a296ce5..024c5d5 100644
--- a/applications/app_common_impl/common_stm32cube.cc
+++ b/applications/app_common_impl/common_stm32cube.cc
@@ -64,8 +64,8 @@
 
 constexpr size_t kNumPixels = kFramebufferWidth * kFramebufferHeight;
 constexpr uint16_t kDisplayRowBytes = sizeof(uint16_t) * kFramebufferWidth;
-constexpr pw::math::Size<uint16_t> kDisplaySize = {DISPLAY_WIDTH,
-                                                   DISPLAY_HEIGHT};
+constexpr pw::geometry::Size<uint16_t> kDisplaySize = {DISPLAY_WIDTH,
+                                                       DISPLAY_HEIGHT};
 constexpr pw::spi::Config kSpiConfig8Bit{
     .polarity = pw::spi::ClockPolarity::kActiveHigh,
     .phase = pw::spi::ClockPhase::kFallingEdge,
diff --git a/applications/terminal_display/BUILD.gn b/applications/terminal_display/BUILD.gn
index 75b8b3f..af0a8cf 100644
--- a/applications/terminal_display/BUILD.gn
+++ b/applications/terminal_display/BUILD.gn
@@ -26,7 +26,7 @@
   public_deps = [
     "$dir_pw_result",
     "$dir_pwexperimental_color",
-    "$dir_pwexperimental_math",
+    "$dir_pwexperimental_geometry",
   ]
   sources = [
     "text_buffer.cc",
@@ -51,7 +51,7 @@
     "$dir_pwexperimental_display",
     "$dir_pwexperimental_draw",
     "$dir_pwexperimental_framebuffer",
-    "$dir_pwexperimental_math",
+    "$dir_pwexperimental_geometry",
   ]
   remove_configs = [ "$dir_pw_build:strict_warnings" ]
 
diff --git a/applications/terminal_display/main.cc b/applications/terminal_display/main.cc
index 47dee57..a56df0a 100644
--- a/applications/terminal_display/main.cc
+++ b/applications/terminal_display/main.cc
@@ -30,12 +30,13 @@
 #include "pw_color/colors_endesga32.h"
 #include "pw_color/colors_pico8.h"
 #include "pw_draw/draw.h"
+#include "pw_draw/font6x8.h"
 #include "pw_draw/font_set.h"
 #include "pw_draw/pigweed_farm.h"
 #include "pw_framebuffer/framebuffer.h"
+#include "pw_geometry/vector2.h"
+#include "pw_geometry/vector3.h"
 #include "pw_log/log.h"
-#include "pw_math/vector2.h"
-#include "pw_math/vector3.h"
 #include "pw_ring_buffer/prefixed_entry_ring_buffer.h"
 #include "pw_spin_delay/delay.h"
 #include "pw_string/string_builder.h"
@@ -48,12 +49,12 @@
 #endif  // if defined(USE_FREERTOS)
 
 using pw::color::color_rgb565_t;
-using pw::color::colors_pico8_rgb565;
+using pw::color::kColorsPico8Rgb565;
 using pw::display::Display;
 using pw::draw::FontSet;
 using pw::framebuffer::Framebuffer;
-using pw::math::Size;
-using pw::math::Vector2;
+using pw::geometry::Size;
+using pw::geometry::Vector2;
 using pw::ring_buffer::PrefixedEntryRingBuffer;
 
 // TODO(cmumford): move this code into a pre_init section (i.e. boot.cc) which
@@ -97,10 +98,10 @@
 
  protected:
   void SetFgColor(uint8_t r, uint8_t g, uint8_t b) override {
-    fg_color_ = pw::color::ColorRGBA(r, g, b).ToRgb565();
+    fg_color_ = pw::color::ColorRgba(r, g, b).ToRgb565();
   }
   void SetBgColor(uint8_t r, uint8_t g, uint8_t b) override {
-    bg_color_ = pw::color::ColorRGBA(r, g, b).ToRgb565();
+    bg_color_ = pw::color::ColorRgba(r, g, b).ToRgb565();
   }
   void EmitChar(char c) override {
     log_text_buffer_.DrawCharacter(TextBuffer::Char{c, fg_color_, bg_color_});
@@ -154,7 +155,7 @@
   constexpr int kMargin = 2;
   Vector2<int> tl{button.tl_.x + kMargin, button.tl_.y + kMargin};
   DrawString(
-      button.label_, tl, kBlack, bg_color, pw::draw::font6x8, framebuffer);
+      button.label_, tl, kBlack, bg_color, pw::draw::GetFont6x8(), framebuffer);
 }
 
 // Draw a font sheet starting at the given top-left screen coordinates.
@@ -237,7 +238,7 @@
       sprite_pos_y - border_size,
       pigweed_farm_sprite_sheet.width * sprite_scale + (border_size * 2),
       pigweed_farm_sprite_sheet.height * sprite_scale + (border_size * 2),
-      colors_pico8_rgb565[COLOR_DARK_BLUE],
+      kColorsPico8Rgb565[pw::color::kColorDarkBlue],
       true);
 
   // Shrink the border
@@ -250,7 +251,7 @@
       sprite_pos_y - border_size,
       pigweed_farm_sprite_sheet.width * sprite_scale + (border_size * 2),
       pigweed_farm_sprite_sheet.height * sprite_scale + (border_size * 2),
-      colors_pico8_rgb565[COLOR_BLUE],
+      kColorsPico8Rgb565[pw::color::kColorBlue],
       true);
 
   static Vector2<int> sun_offset;
@@ -273,7 +274,7 @@
                            32,
                        sun_offset.y + sprite_pos_y,
                        20,
-                       colors_pico8_rgb565[COLOR_ORANGE],
+                       kColorsPico8Rgb565[pw::color::kColorOrange],
                        true);
   pw::draw::DrawCircle(framebuffer,
                        sun_offset.x + sprite_pos_x +
@@ -281,7 +282,7 @@
                            32,
                        sun_offset.y + sprite_pos_y,
                        18,
-                       colors_pico8_rgb565[COLOR_YELLOW],
+                       kColorsPico8Rgb565[pw::color::kColorYellow],
                        true);
 
   // Draw the farm sprite's shadow
@@ -308,9 +309,9 @@
 
   DrawString(fps_msg,
              tl,
-             colors_pico8_rgb565[COLOR_PEACH],
+             kColorsPico8Rgb565[pw::color::kColorPeach],
              kBlack,
-             pw::draw::font6x8,
+             pw::draw::GetFont6x8(),
              framebuffer);
 }
 
@@ -324,17 +325,19 @@
       L" ▒█▀     ░█░ ▓█   █▓ ░█░ █ ▒█  ▒█   ▄  ▒█   ▄  ░█  ▄█▌",
       L" ▒█      ░█░ ░▓███▀   ▒█▓▀▓█░ ░▓████▒ ░▓████▒ ▒▓████▀"};
 
+  auto font = pw::draw::GetFont6x8BoxChars();
   // Draw the Pigweed "ASCII" banner.
   for (auto text_row : pigweed_banner) {
-    Size<int> string_dims = DrawString(text_row,
-                                       tl,
-                                       colors_pico8_rgb565[COLOR_PINK],
-                                       kBlack,
-                                       pw::draw::font6x8_box_chars,
-                                       framebuffer);
+    Size<int> string_dims =
+        DrawString(text_row,
+                   tl,
+                   kColorsPico8Rgb565[pw::color::kColorPink],
+                   kBlack,
+                   font,
+                   framebuffer);
     tl.y += string_dims.height;
   }
-  return tl.y - pw::draw::font6x8_box_chars.height;
+  return tl.y - font.height;
 }
 
 // Draw the font sheets.
@@ -343,22 +346,23 @@
   constexpr int kFontSheetVerticalPadding = 4;
   constexpr int kFontSheetNumColumns = 48;
 
+  auto font = pw::draw::GetFont6x8();
   int initial_x = tl.x;
   tl = DrawColorFontSheet(tl,
                           kFontSheetNumColumns,
                           /*fg_color=*/kBlack,
-                          pw::draw::font6x8,
+                          font,
                           framebuffer);
 
   tl.x = initial_x;
-  tl.y -= pw::draw::font6x8.height;
+  tl.y -= font.height;
   tl.y += kFontSheetVerticalPadding;
 
   tl = DrawTestFontSheet(tl,
                          kFontSheetNumColumns,
                          /*fg_color=*/kWhite,
                          /*bg_color=*/kBlack,
-                         pw::draw::font6x8,
+                         font,
                          framebuffer);
 
   tl.x = initial_x;
@@ -368,16 +372,16 @@
                                      tl,
                                      /*fg_color=*/kWhite,
                                      /*bg_color=*/kBlack,
-                                     pw::draw::font6x8,
+                                     font,
                                      framebuffer);
-  tl.x += string_dims.width + pw::draw::font6x8.width;
-  tl.y -= pw::draw::font6x8.height;
+  tl.x += string_dims.width + font.width;
+  tl.y -= font.height;
 
   tl = DrawTestFontSheet(tl,
                          /*num_columns=*/32,
                          /*fg_color=*/kWhite,
                          /*bg_color=*/kBlack,
-                         pw::draw::font6x8_box_chars,
+                         pw::draw::GetFont6x8BoxChars(),
                          framebuffer);
   return tl.y;
 }
@@ -385,8 +389,9 @@
 // Draw the application header section which is mostly static text/graphics.
 // Return the height (in pixels) of the header.
 int DrawHeader(Framebuffer& framebuffer, std::wstring_view fps_msg) {
-  DrawButton(
-      g_button, /*bg_color=*/colors_pico8_rgb565[COLOR_BLUE], framebuffer);
+  DrawButton(g_button,
+             /*bg_color=*/kColorsPico8Rgb565[pw::color::kColorBlue],
+             framebuffer);
   Vector2<int> tl = {0, 0};
   tl.y = DrawPigweedSprite(framebuffer);
 
@@ -426,7 +431,7 @@
   constexpr int kHeaderMargin = 4;
   int header_bottom = DrawHeader(framebuffer, fps_msg);
   DrawLogTextBuffer(
-      header_bottom + kHeaderMargin, pw::draw::font6x8, framebuffer);
+      header_bottom + kHeaderMargin, pw::draw::GetFont6x8(), framebuffer);
 }
 
 void CreateDemoLogMessages() {
diff --git a/applications/terminal_display/text_buffer.cc b/applications/terminal_display/text_buffer.cc
index c3e42e5..426172a 100644
--- a/applications/terminal_display/text_buffer.cc
+++ b/applications/terminal_display/text_buffer.cc
@@ -15,7 +15,7 @@
 #include "text_buffer.h"
 
 using pw::color::color_rgb565_t;
-using pw::math::Vector2;
+using pw::geometry::Vector2;
 
 namespace {
 constexpr int kMaxColIdx = kNumCharsWide - 1;
diff --git a/applications/terminal_display/text_buffer.h b/applications/terminal_display/text_buffer.h
index d40faf7..039a1cd 100644
--- a/applications/terminal_display/text_buffer.h
+++ b/applications/terminal_display/text_buffer.h
@@ -16,8 +16,8 @@
 #include <array>
 
 #include "pw_color/color.h"
-#include "pw_math/size.h"
-#include "pw_math/vector2.h"
+#include "pw_geometry/size.h"
+#include "pw_geometry/vector2.h"
 #include "pw_result/result.h"
 
 constexpr size_t kNumCharsWide = 52;
@@ -62,18 +62,18 @@
   void DrawCharacter(const Char& ch);
 
   // Return the size, in characters, of the text buffer.
-  pw::math::Size<int> GetSize() const {
-    return pw::math::Size<int>{kNumCharsWide, kNumRows};
+  pw::geometry::Size<int> GetSize() const {
+    return pw::geometry::Size<int>{kNumCharsWide, kNumRows};
   }
 
   // Return the character at the specified location.
-  pw::Result<Char> GetChar(pw::math::Vector2<int> loc) const;
+  pw::Result<Char> GetChar(pw::geometry::Vector2<int> loc) const;
 
  private:
   void ScrollUp();
   void InsertNewline();
 
-  pw::math::Vector2<int> cursor_ = {0, 0};
+  pw::geometry::Vector2<int> cursor_ = {0, 0};
   bool character_wrap_enabled_ = false;
   std::array<TextRow, kNumRows> text_rows_;
 };
diff --git a/applications/terminal_display/text_buffer_test.cc b/applications/terminal_display/text_buffer_test.cc
index c962f14..1140cfa 100644
--- a/applications/terminal_display/text_buffer_test.cc
+++ b/applications/terminal_display/text_buffer_test.cc
@@ -20,8 +20,8 @@
 using pw::color::color_rgb565_t;
 
 namespace {
-constexpr color_rgb565_t kIndigo = pw::color::colors_pico8_rgb565[13];
-constexpr color_rgb565_t kDarkGreen = pw::color::colors_pico8_rgb565[3];
+constexpr color_rgb565_t kIndigo = pw::color::kColorsPico8Rgb565[13];
+constexpr color_rgb565_t kDarkGreen = pw::color::kColorsPico8Rgb565[3];
 }  // namespace
 
 TEST(TextBufferTest, DimsAsExpected) {
diff --git a/modules.gni b/modules.gni
index b3a1942..9f4de06 100644
--- a/modules.gni
+++ b/modules.gni
@@ -29,7 +29,8 @@
   dir_pw_board_led_stm32cube =
       get_path_info("pw_board_led_stm32cube", "abspath")
   dir_pwexperimental_color = get_path_info("pw_graphics/pw_color", "abspath")
-  dir_pwexperimental_math = get_path_info("pw_graphics/pw_math", "abspath")
+  dir_pwexperimental_geometry =
+      get_path_info("pw_graphics/pw_geometry", "abspath")
   dir_pw_digital_io_arduino = get_path_info("pw_digital_io_arduino", "abspath")
   dir_pw_digital_io_stm32cube =
       get_path_info("pw_digital_io_stm32cube", "abspath")
diff --git a/pw_display_driver/BUILD.bazel b/pw_display_driver/BUILD.bazel
new file mode 100644
index 0000000..8bb0fe8
--- /dev/null
+++ b/pw_display_driver/BUILD.bazel
@@ -0,0 +1,29 @@
+# Copyright 2024 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.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "pw_display_driver",
+    hdrs = ["public/pw_display_driver/display_driver.h"],
+    includes = ["public"],
+    deps = [
+        "//pw_graphics/pw_framebuffer",
+        "@pigweed//pw_function",
+        "@pigweed//pw_span",
+        "@pigweed//pw_status",
+    ],
+)
diff --git a/pw_display_driver/BUILD.gn b/pw_display_driver/BUILD.gn
index 34f63b2..b0d3e4a 100644
--- a/pw_display_driver/BUILD.gn
+++ b/pw_display_driver/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2024 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
@@ -23,11 +23,10 @@
   visibility = [ ":*" ]
 }
 
-group("pw_display_driver") {
-  deps = [ ":display_driver" ]
+pw_test_group("tests") {
 }
 
-pw_source_set("display_driver") {
+pw_source_set("pw_display_driver") {
   public_configs = [ ":public_include_path" ]
   public = [ "public/pw_display_driver/display_driver.h" ]
   public_deps = [
@@ -38,9 +37,6 @@
   ]
 }
 
-pw_test("display_driver_test") {
-}
-
 pw_doc_group("docs") {
   sources = [ "docs.rst" ]
 }
diff --git a/pw_display_driver/CMakeLists.txt b/pw_display_driver/CMakeLists.txt
new file mode 100644
index 0000000..b3cc903
--- /dev/null
+++ b/pw_display_driver/CMakeLists.txt
@@ -0,0 +1,29 @@
+# Copyright 2024 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_display_driver INTERFACE
+  HEADERS
+    public/pw_display_driver/display_driver.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_framebuffer
+    pw_function
+    pw_span
+    pw_status
+)
+
+# CMake does not yet support building docs.
diff --git a/pw_display_driver/docs.rst b/pw_display_driver/docs.rst
index e69de29..5e01ead 100644
--- a/pw_display_driver/docs.rst
+++ b/pw_display_driver/docs.rst
@@ -0,0 +1,20 @@
+.. _module-pw_display_driver:
+
+=================
+pw_display_driver
+=================
+.. pigweed-module::
+   :name: pw_display_driver
+
+.. seealso::
+   This module is part of SEED :ref:`seed-0104`.
+
+-------------
+API reference
+-------------
+.. doxygengroup:: pw_display_driver
+   :members:
+
+.. include:: ../pw_display/docs.rst
+   :start-after: .. graphics-modules-start
+   :end-before: .. graphics-modules-end
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 95d6283..cd84aa0 100644
--- a/pw_display_driver/public/pw_display_driver/display_driver.h
+++ b/pw_display_driver/public/pw_display_driver/display_driver.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -21,12 +21,16 @@
 #include "pw_span/span.h"
 #include "pw_status/status.h"
 
+/// @defgroup pw_display_driver
+
 namespace pw::display_driver {
 
-// This interface defines a software display driver. This is the software
-// component responsible for all communications with a display controller.
-// The display controller is the hardware component of a display that
-// controls pixel values and other physical display properties.
+/// @ingroup pw_display_driver
+///
+/// This interface defines a software display driver. This is the software
+/// component responsible for all communications with a display controller.
+/// The display controller is the hardware component of a display that
+/// controls pixel values and other physical display properties.
 class DisplayDriver {
  public:
   // Called on the completion of a write operation.
@@ -34,16 +38,16 @@
 
   virtual ~DisplayDriver() = default;
 
-  // Initialize the display controller.
+  /// Initialize the display controller.
   virtual Status Init() = 0;
 
-  // Send all pixels in the supplied |framebuffer| to the display controller
-  // for display.
+  /// Send all pixels in the supplied |framebuffer| to the display controller
+  /// for display.
   virtual void WriteFramebuffer(pw::framebuffer::Framebuffer framebuffer,
                                 WriteCallback write_callback) = 0;
 
-  // Send a row of pixels to the display. The number of pixels must be <=
-  // display width.
+  /// Send a row of pixels to the display. The number of pixels must be <=
+  /// display width.
   virtual Status WriteRow(span<uint16_t> row_pixels,
                           uint16_t row_idx,
                           uint16_t col_idx) = 0;
@@ -52,7 +56,7 @@
 
   virtual uint16_t GetHeight() const = 0;
 
-  // Display driver supports resizing during write.
+  /// Display driver supports resizing during write.
   virtual bool SupportsResize() const { return false; }
 };
 
diff --git a/pw_display_driver_ili9341/BUILD.gn b/pw_display_driver_ili9341/BUILD.gn
index 7c3a007..7e21d11 100644
--- a/pw_display_driver_ili9341/BUILD.gn
+++ b/pw_display_driver_ili9341/BUILD.gn
@@ -43,9 +43,9 @@
   public_deps = [
     "$dir_pw_digital_io",
     "$dir_pw_spi:device",
-    "$dir_pwexperimental_display_driver:display_driver",
+    "$dir_pwexperimental_display_driver",
     "$dir_pwexperimental_framebuffer_pool",
-    "$dir_pwexperimental_pixel_pusher:pixel_pusher",
+    "$dir_pwexperimental_pixel_pusher",
   ]
   sources = [ "display_driver.cc" ]
   remove_configs = [ "$dir_pw_build:strict_warnings" ]
diff --git a/pw_display_driver_imgui/BUILD.gn b/pw_display_driver_imgui/BUILD.gn
index 8f679bd..a9c6f29 100644
--- a/pw_display_driver_imgui/BUILD.gn
+++ b/pw_display_driver_imgui/BUILD.gn
@@ -30,10 +30,10 @@
     "$dir_pigweed_experimental/third_party/glfw",
     "$dir_pigweed_experimental/third_party/imgui",
     "$dir_pw_log",
-    "$dir_pwexperimental_math",
+    "$dir_pwexperimental_geometry",
   ]
   public_deps = [
-    "$dir_pwexperimental_display_driver:display_driver",
+    "$dir_pwexperimental_display_driver",
     "$dir_pwexperimental_framebuffer_pool",
   ]
   sources = [ "display_driver.cc" ]
diff --git a/pw_display_driver_imgui/display_driver.cc b/pw_display_driver_imgui/display_driver.cc
index bdc3690..a165a45 100644
--- a/pw_display_driver_imgui/display_driver.cc
+++ b/pw_display_driver_imgui/display_driver.cc
@@ -97,7 +97,7 @@
 }
 
 void _SetTexturePixel(GLuint x, GLuint y, color_rgb565_t rgb565) {
-  pw::color::ColorRGBA c(rgb565);
+  pw::color::ColorRgba c(rgb565);
   _SetTexturePixel(x, y, c.r, c.g, c.b, 255);
 }
 
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 6e6c33e..9f0ea1c 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
@@ -17,7 +17,7 @@
 
 #include "pw_display_driver/display_driver.h"
 #include "pw_framebuffer_pool/framebuffer_pool.h"
-#include "pw_math/vector3.h"
+#include "pw_geometry/vector3.h"
 
 struct ImGuiMousePosition {
   bool left_button_pressed = false;
diff --git a/pw_display_driver_mipi/BUILD.gn b/pw_display_driver_mipi/BUILD.gn
index 6228daf..108f070 100644
--- a/pw_display_driver_mipi/BUILD.gn
+++ b/pw_display_driver_mipi/BUILD.gn
@@ -27,8 +27,8 @@
   public_deps = [
     "$dir_pw_spi:device",
     "$dir_pw_status",
-    "$dir_pwexperimental_display_driver:display_driver",
-    "$dir_pwexperimental_math",
+    "$dir_pwexperimental_display_driver",
+    "$dir_pwexperimental_geometry",
     "$dir_pwexperimental_mipi_dsi",
   ]
   sources = [ "display_driver.cc" ]
diff --git a/pw_display_driver_mipi/display_driver.cc b/pw_display_driver_mipi/display_driver.cc
index 880b667..bb2e236 100644
--- a/pw_display_driver_mipi/display_driver.cc
+++ b/pw_display_driver_mipi/display_driver.cc
@@ -21,7 +21,7 @@
 pw::mipi::dsi::Device::WriteCallback s_write_callback;
 
 DisplayDriverMipiDsi::DisplayDriverMipiDsi(
-    pw::mipi::dsi::Device& device, pw::math::Size<uint16_t> display_size)
+    pw::mipi::dsi::Device& device, pw::geometry::Size<uint16_t> display_size)
     : device_(device), display_size_(display_size) {}
 
 DisplayDriverMipiDsi::~DisplayDriverMipiDsi() = default;
diff --git a/pw_display_driver_mipi/public/pw_display_driver_mipi/display_driver.h b/pw_display_driver_mipi/public/pw_display_driver_mipi/display_driver.h
index 34bcd6a..48f94db 100644
--- a/pw_display_driver_mipi/public/pw_display_driver_mipi/display_driver.h
+++ b/pw_display_driver_mipi/public/pw_display_driver_mipi/display_driver.h
@@ -14,7 +14,7 @@
 #pragma once
 
 #include "pw_display_driver/display_driver.h"
-#include "pw_math/size.h"
+#include "pw_geometry/size.h"
 #include "pw_mipi_dsi/device.h"
 
 namespace pw::display_driver {
@@ -24,7 +24,7 @@
 class DisplayDriverMipiDsi : public DisplayDriver {
  public:
   DisplayDriverMipiDsi(pw::mipi::dsi::Device& device,
-                       pw::math::Size<uint16_t> display_size);
+                       pw::geometry::Size<uint16_t> display_size);
   ~DisplayDriverMipiDsi() override;
 
   // pw::display_driver::DisplayDriver implementation:
@@ -39,7 +39,7 @@
 
  private:
   pw::mipi::dsi::Device& device_;
-  const pw::math::Size<uint16_t> display_size_;
+  const pw::geometry::Size<uint16_t> display_size_;
 };
 
 }  // namespace pw::display_driver
diff --git a/pw_display_driver_null/BUILD.bazel b/pw_display_driver_null/BUILD.bazel
new file mode 100644
index 0000000..5662307
--- /dev/null
+++ b/pw_display_driver_null/BUILD.bazel
@@ -0,0 +1,26 @@
+# Copyright 2024 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.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "pw_display_driver_null",
+    hdrs = ["public/pw_display_driver_null/display_driver_null.h"],
+    includes = ["public"],
+    deps = [
+        "//pw_display_driver",
+    ],
+)
diff --git a/pw_display_driver_null/BUILD.gn b/pw_display_driver_null/BUILD.gn
index e84150e..50d10d8 100644
--- a/pw_display_driver_null/BUILD.gn
+++ b/pw_display_driver_null/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2024 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
@@ -15,14 +15,22 @@
 import("//build_overrides/pigweed.gni")
 
 import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
 
-config("default_config") {
+config("public_include_path") {
   include_dirs = [ "public" ]
 }
 
 pw_source_set("pw_display_driver_null") {
-  public_configs = [ ":default_config" ]
+  public_configs = [ ":public_include_path" ]
   public = [ "public/pw_display_driver_null/display_driver.h" ]
-  public_deps = [ "$dir_pwexperimental_display_driver:display_driver" ]
-  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+  public_deps = [ "$dir_pwexperimental_display_driver" ]
+}
+
+pw_test_group("tests") {
+}
+
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
 }
diff --git a/pw_display_driver_null/CMakeLists.txt b/pw_display_driver_null/CMakeLists.txt
new file mode 100644
index 0000000..6dde64f
--- /dev/null
+++ b/pw_display_driver_null/CMakeLists.txt
@@ -0,0 +1,24 @@
+# Copyright 2024 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_display_driver_null INTERFACE
+  HEADERS
+    public/pw_display_driver_null/display_driver.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_display_driver
+)
diff --git a/pw_display_driver_null/docs.rst b/pw_display_driver_null/docs.rst
new file mode 100644
index 0000000..950c02d
--- /dev/null
+++ b/pw_display_driver_null/docs.rst
@@ -0,0 +1,26 @@
+.. _module-pw_display_driver_null:
+
+======================
+pw_display_driver_null
+======================
+.. pigweed-module::
+   :name: pw_display_driver_null
+
+.. seealso::
+   This module is part of SEED :ref:`seed-0104`.
+
+``pw_display_driver_null`` is a :ref:`module-pw_display_driver` backend that
+ignores all display hardware management.
+
+This backend can be used to completely disable display hardware code which may
+be helpful for testing or bring-up situations.
+
+-------------
+API reference
+-------------
+.. doxygengroup:: pw_display_driver_null
+   :members:
+
+.. include:: ../pw_display/docs.rst
+   :start-after: .. graphics-modules-start
+   :end-before: .. graphics-modules-end
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 5113dac..229c961 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
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -17,15 +17,19 @@
 
 #include "pw_display_driver/display_driver.h"
 
+/// @defgroup pw_display_driver_null
+
 namespace pw::display_driver {
 
-// A no-op display driver that does nothing. Handy for display-less targets and
-// testing.
+/// @ingroup pw_display_driver_null
+///
+/// A no-op display driver that does nothing. Handy for display-less targets and
+/// testing.
 class DisplayDriverNULL : public DisplayDriver {
  public:
   ~DisplayDriverNULL() override = default;
 
-  // pw::display_driver::DisplayDriver implementation:
+  /// pw::display_driver::DisplayDriver implementation:
   pw::Status Init() override { return pw::OkStatus(); }
   void WriteFramebuffer(pw::framebuffer::Framebuffer framebuffer,
                         WriteCallback write_callback) override {
diff --git a/pw_display_driver_st7735/BUILD.gn b/pw_display_driver_st7735/BUILD.gn
index 2e03b94..7e32c9b 100644
--- a/pw_display_driver_st7735/BUILD.gn
+++ b/pw_display_driver_st7735/BUILD.gn
@@ -31,7 +31,7 @@
   public_deps = [
     "$dir_pw_digital_io",
     "$dir_pw_spi:device",
-    "$dir_pwexperimental_display_driver:display_driver",
+    "$dir_pwexperimental_display_driver",
   ]
   sources = [ "display_driver.cc" ]
   remove_configs = [ "$dir_pw_build:strict_warnings" ]
diff --git a/pw_display_driver_st7789/BUILD.bazel b/pw_display_driver_st7789/BUILD.bazel
new file mode 100644
index 0000000..373b7a9
--- /dev/null
+++ b/pw_display_driver_st7789/BUILD.bazel
@@ -0,0 +1,33 @@
+# Copyright 2024 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.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "pw_display_driver_st7789",
+    srcs = ["display_driver.cc"],
+    hdrs = ["public/pw_display_driver_st7789/display_driver.h"],
+    includes = ["public"],
+    deps = [
+        "//pw_display_driver",
+        "//pw_pixel_pusher",
+        "@pigweed//pw_chrono:system_clock",
+        "@pigweed//pw_digital_io",
+        "@pigweed//pw_log",
+        "@pigweed//pw_spi:device",
+        "@pigweed//pw_thread:sleep",
+    ],
+)
diff --git a/pw_display_driver_st7789/BUILD.gn b/pw_display_driver_st7789/BUILD.gn
index 7668391..26ae4bd 100644
--- a/pw_display_driver_st7789/BUILD.gn
+++ b/pw_display_driver_st7789/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2024 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
@@ -15,25 +15,34 @@
 import("//build_overrides/pigweed.gni")
 
 import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
 
-config("default_config") {
+config("public_include_path") {
   include_dirs = [ "public" ]
+  visibility = [ ":*" ]
+}
+
+pw_test_group("tests") {
 }
 
 pw_source_set("pw_display_driver_st7789") {
-  public_configs = [ ":default_config" ]
+  public_configs = [ ":public_include_path" ]
   public = [ "public/pw_display_driver_st7789/display_driver.h" ]
+  sources = [ "display_driver.cc" ]
   deps = [
     "$dir_pw_chrono:system_clock",
     "$dir_pw_log",
-    "$dir_pw_thread:thread",
+    "$dir_pw_thread:sleep",
   ]
   public_deps = [
     "$dir_pw_digital_io",
     "$dir_pw_spi:device",
-    "$dir_pwexperimental_display_driver:display_driver",
-    "$dir_pwexperimental_pixel_pusher:pixel_pusher",
+    "$dir_pwexperimental_display_driver",
+    "$dir_pwexperimental_pixel_pusher",
   ]
-  sources = [ "display_driver.cc" ]
-  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
+
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
 }
diff --git a/pw_display_driver_st7789/CMakeLists.txt b/pw_display_driver_st7789/CMakeLists.txt
new file mode 100644
index 0000000..77b34ea
--- /dev/null
+++ b/pw_display_driver_st7789/CMakeLists.txt
@@ -0,0 +1,32 @@
+# Copyright 2024 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_display_driver_st7789 STATIC
+  SOURCES
+    display_driver.cc
+  HEADERS
+    public/pw_display_driver_st7789/display_driver.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_chrono.system_clock
+    pw_digital_io
+    pw_display_driver
+    pw_log
+    pw_pixel_pusher
+    pw_spi.device
+    pw_thread.sleep
+)
diff --git a/pw_display_driver_st7789/display_driver.cc b/pw_display_driver_st7789/display_driver.cc
index 381dce0..592d441 100644
--- a/pw_display_driver_st7789/display_driver.cc
+++ b/pw_display_driver_st7789/display_driver.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
diff --git a/pw_display_driver_st7789/docs.rst b/pw_display_driver_st7789/docs.rst
new file mode 100644
index 0000000..748f5a7
--- /dev/null
+++ b/pw_display_driver_st7789/docs.rst
@@ -0,0 +1,20 @@
+.. _module-pw_display_driver_st7789:
+
+========================
+pw_display_driver_st7789
+========================
+.. pigweed-module::
+   :name: pw_display_driver_st7789
+
+.. seealso::
+   This module is part of SEED :ref:`seed-0104`.
+
+-------------
+API reference
+-------------
+.. doxygengroup:: pw_display_driver_st7789
+   :members:
+
+.. include:: ../pw_display/docs.rst
+   :start-after: .. graphics-modules-start
+   :end-before: .. graphics-modules-end
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 5f13d65..b3b079a 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
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -20,8 +20,14 @@
 #include "pw_pixel_pusher/pixel_pusher.h"
 #include "pw_spi/device.h"
 
+/// @defgroup pw_display_driver_st7789
+
 namespace pw::display_driver {
 
+/// @ingroup pw_display_driver_st7789
+///
+/// Implementation of pw_display_driver for an ST7789 display controller
+/// connected over SPI.
 class DisplayDriverST7789 : public DisplayDriver {
  public:
   // DisplayDriverST7789 configuration parameters.
diff --git a/pw_graphics/pw_color/BUILD.bazel b/pw_graphics/pw_color/BUILD.bazel
index 93bb5b7..47a7178 100644
--- a/pw_graphics/pw_color/BUILD.bazel
+++ b/pw_graphics/pw_color/BUILD.bazel
@@ -1,4 +1,4 @@
-# Copyright 2023 The Pigweed Authors
+# Copyright 2024 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
@@ -13,11 +13,12 @@
 # the License.
 load("@pigweed//pw_build:pigweed.bzl", "pw_cc_test")
 
+package(default_visibility = ["//visibility:public"])
+
 cc_library(
     name = "pw_color",
     hdrs = [
         "public/pw_color/color.h",
-        "public/pw_color/colors_endesga32.h",
         "public/pw_color/colors_pico8.h",
     ],
     includes = ["public"],
@@ -26,8 +27,5 @@
 pw_cc_test(
     name = "color_test",
     srcs = ["color_test.cc"],
-    deps = [
-        ":pw_color",
-        "@pigweed//pw_log",
-    ],
+    deps = [":pw_color"],
 )
diff --git a/pw_graphics/pw_color/BUILD.gn b/pw_graphics/pw_color/BUILD.gn
index 16a4edc..4cb8f3d 100644
--- a/pw_graphics/pw_color/BUILD.gn
+++ b/pw_graphics/pw_color/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2024 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
@@ -18,27 +18,27 @@
 import("$dir_pw_docgen/docs.gni")
 import("$dir_pw_unit_test/test.gni")
 
-config("default_config") {
+config("public_include_path") {
   include_dirs = [ "public" ]
 }
 
 pw_source_set("pw_color") {
-  public_configs = [ ":default_config" ]
+  public_configs = [ ":public_include_path" ]
   public = [
     "public/pw_color/color.h",
-    "public/pw_color/colors_endesga32.h",
     "public/pw_color/colors_pico8.h",
   ]
 }
 
 pw_test("color_test") {
-  deps = [
-    ":pw_color",
-    "$dir_pw_log",
-  ]
+  deps = [ ":pw_color" ]
   sources = [ "color_test.cc" ]
 }
 
 pw_test_group("tests") {
   tests = [ ":color_test" ]
 }
+
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
+}
diff --git a/pw_graphics/pw_color/CMakeLists.txt b/pw_graphics/pw_color/CMakeLists.txt
new file mode 100644
index 0000000..ee408ce
--- /dev/null
+++ b/pw_graphics/pw_color/CMakeLists.txt
@@ -0,0 +1,33 @@
+# Copyright 2024 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_color INTERFACE
+  HEADERS
+    public/pw_color/color.h
+    public/pw_color/colors_pico8.h
+  PUBLIC_INCLUDES
+    public
+)
+
+pw_add_test(pw_color.color_test
+  SOURCES
+    color_test.cc
+  PRIVATE_DEPS
+    pw_color
+  GROUPS
+    modules
+    pw_color
+)
diff --git a/pw_graphics/pw_color/color_test.cc b/pw_graphics/pw_color/color_test.cc
index cc8b080..128efd1 100644
--- a/pw_graphics/pw_color/color_test.cc
+++ b/pw_graphics/pw_color/color_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -15,44 +15,38 @@
 #include "pw_color/color.h"
 
 #include "gtest/gtest.h"
-#include "pw_color/colors_endesga32.h"
 #include "pw_color/colors_pico8.h"
-#include "pw_log/log.h"
 
 namespace pw::color {
 namespace {
 
-TEST(ColorsPico8Rgb565, Exists) { EXPECT_EQ(colors_pico8_rgb565[1], 0x194a); }
-
-TEST(ColorsEndesga32Rgb565, Exists) {
-  EXPECT_EQ(colors_endesga32_rgb565[1], 0xd3a8);
+TEST(ColorsPico8Rgb565, Exists) {
+  EXPECT_EQ(kColorsPico8Rgb565[kColorDarkBlue], 0x194a);
 }
 
 TEST(ColorToRGB565, FromRGB) {
-  EXPECT_EQ(ColorRGBA(0x1d, 0x2b, 0x53).ToRgb565(), colors_pico8_rgb565[1]);
+  EXPECT_EQ(ColorRgba(0x1d, 0x2b, 0x53).ToRgb565(),
+            kColorsPico8Rgb565[kColorDarkBlue]);
 }
 
 TEST(ColorToRGB565, FromRGBA) {
-  // color_rgba8888_t dark_blue = 0xff532b1d;  // A B G R
-  EXPECT_EQ(ColorRGBA(colors_pico8_rgba8888[1]).ToRgb565(),
-            colors_pico8_rgb565[1]);
+  EXPECT_EQ(ColorRgba(kColorsPico8Rgba8888[kColorDarkBlue]).ToRgb565(),
+            kColorsPico8Rgb565[kColorDarkBlue]);
 }
 
-TEST(SplitColor, FromRGBA888) {
-  // color_rgba8888_t dark_blue = 0xff532b1d;  // A B G R
-  ColorRGBA color(colors_pico8_rgba8888[13]);
+TEST(ConvertColor, FromRGBA888) {
+  ColorRgba color(kColorsPico8Rgba8888[kColorIndigo]);
   EXPECT_EQ(color.a, 0xff);
   EXPECT_EQ(color.r, 0x83);
   EXPECT_EQ(color.g, 0x76);
   EXPECT_EQ(color.b, 0x9c);
 }
 
-TEST(SplitColor, FromRGB565) {
-  // color_rgba8888_t dark_blue = 0xff532b1d;  // A B G R
-  ColorRGBA color(colors_pico8_rgb565[13]);
+TEST(ConvertColor, FromRGB565) {
+  ColorRgba color(kColorsPico8Rgb565[kColorIndigo]);
   EXPECT_EQ(color.a, 0xff);
-  EXPECT_EQ(color.r, 0x84);  // Slightly off
-  EXPECT_EQ(color.g, 0x75);  // Slightly off
+  EXPECT_EQ(color.r, 0x83);
+  EXPECT_EQ(color.g, 0x75);
   EXPECT_EQ(color.b, 0x9c);
 }
 
diff --git a/pw_graphics/pw_color/docs.rst b/pw_graphics/pw_color/docs.rst
new file mode 100644
index 0000000..13e2737
--- /dev/null
+++ b/pw_graphics/pw_color/docs.rst
@@ -0,0 +1,22 @@
+.. _module-pw_color:
+
+========
+pw_color
+========
+.. pigweed-module::
+   :name: pw_color
+
+.. seealso::
+   This module is part of SEED :ref:`seed-0104`.
+
+A small module for convertng between RGB values and rgb565, rgba8888 formats.
+
+-------------
+API reference
+-------------
+.. doxygengroup:: pw_color
+   :members:
+
+.. include:: ../pw_display/docs.rst
+   :start-after: .. graphics-modules-start
+   :end-before: .. graphics-modules-end
diff --git a/pw_graphics/pw_color/public/pw_color/color.h b/pw_graphics/pw_color/public/pw_color/color.h
index 4ca468d..252f3a1 100644
--- a/pw_graphics/pw_color/public/pw_color/color.h
+++ b/pw_graphics/pw_color/public/pw_color/color.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -19,66 +19,73 @@
 #include <cinttypes>
 #include <cstdint>
 
+/// @defgroup pw_color
+
 namespace pw::color {
 
+/// @ingroup pw_color
+///
+/// Base type for rgb8888.
 typedef uint32_t color_rgba8888_t;
-typedef uint16_t color_rgb565_t;
-typedef uint8_t color_1bit_t;
-typedef uint8_t color_2bit_t;
 
-class ColorRGBA {
+/// @ingroup pw_color
+///
+/// Base type for rgb564.
+typedef uint16_t color_rgb565_t;
+
+/// @ingroup pw_color
+///
+/// Class for converting between RGB values, RGB565 and RGBA8888 formats.
+class ColorRgba {
  public:
   uint8_t r;
   uint8_t g;
   uint8_t b;
   uint8_t a;
 
-  ColorRGBA(uint8_t ir, uint8_t ig, uint8_t ib) {
-    r = ir;
-    g = ig;
-    b = ib;
+  /// Instantiate from individual red, green and blue unsigned integers. Alpha
+  /// is set to the max of 255.
+  ColorRgba(uint8_t red, uint8_t green, uint8_t blue) {
+    r = red;
+    g = green;
+    b = blue;
     a = 255;
   }
 
-  ColorRGBA(uint8_t ir, uint8_t ig, uint8_t ib, uint8_t ia) {
-    r = ir;
-    g = ig;
-    b = ib;
-    a = ia;
+  /// Instantiate from individual red, green, blue and alpha unsigned integers.
+  ColorRgba(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha) {
+    r = red;
+    g = green;
+    b = blue;
+    a = alpha;
   }
 
-  ColorRGBA(color_rgb565_t rgb565) {
+  /// Instantiate from a 16 bit rgb565 value. This will scale each color to 8
+  /// bits per pixel.
+  ColorRgba(color_rgb565_t rgb565) {
     // Grab the RGB bits
     uint8_t ir = (rgb565 & 0xF800) >> 11;
     uint8_t ig = (rgb565 & 0x7E0) >> 5;
     uint8_t ib = rgb565 & 0x1F;
     // Scale RGB values to 8bits each
-    r = round(255.0 * ((float)ir / 31.0));
-    g = round(255.0 * ((float)ig / 63.0));
-    b = round(255.0 * ((float)ib / 31.0));
+    r = 255 * ir / 31;
+    g = 255 * ig / 63;
+    b = 255 * ib / 31;
     a = 255;
   }
 
-  ColorRGBA(color_rgba8888_t rgba8888) {
+  /// Instantiate from a 32 bit rgb8888 value.
+  ColorRgba(color_rgba8888_t rgba8888) {
     a = (rgba8888 & 0xFF000000) >> 24;
     b = (rgba8888 & 0xFF0000) >> 16;
     g = (rgba8888 & 0xFF00) >> 8;
     r = (rgba8888 & 0xFF);
   }
 
+  /// Return a 16 bit rgb565 value.
   color_rgb565_t ToRgb565() {
     return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3);
   }
-
-  /*
-color_rgb565_t ColorToRGB565(color_rgba8888_t rgba8888) {
-  uint8_t b = (rgba8888 & 0xFF0000) >> 16;
-  uint8_t g = (rgba8888 & 0xFF00) >> 8;
-  uint8_t r = (rgba8888 & 0xFF);
-  return (color_rgb565_t)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) |
-                          ((b & 0xF8) >> 3));
-}
-  */
 };
 
 }  // namespace pw::color
diff --git a/pw_graphics/pw_color/public/pw_color/colors_pico8.h b/pw_graphics/pw_color/public/pw_color/colors_pico8.h
index e3bf15f..3a4f5f9 100644
--- a/pw_graphics/pw_color/public/pw_color/colors_pico8.h
+++ b/pw_graphics/pw_color/public/pw_color/colors_pico8.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -19,59 +19,72 @@
 
 namespace pw::color {
 
-constexpr color_rgb565_t colors_pico8_rgb565[] = {
-    0x0000,  // #000000 0 BLACK
-    0x194a,  // #1d2b53 1 DARK_BLUE
-    0x792a,  // #7e2553 2 DARK_PURPLE
-    0x042a,  // #008751 3 DARK_GREEN
-    0xaa86,  // #ab5236 4 BROWN
-    0x5aa9,  // #5f574f 5 DARK_GRAY
-    0xc618,  // #c2c3c7 6 LIGHT_GRAY
-    0xff9d,  // #fff1e8 7 WHITE
-    0xf809,  // #ff004d 8 RED
-    0xfd00,  // #ffa300 9 ORANGE
-    0xff64,  // #ffec27 10 YELLOW
-    0x0726,  // #00e436 11 GREEN
-    0x2d7f,  // #29adff 12 BLUE
-    0x83b3,  // #83769c 13 INDIGO
-    0xfbb5,  // #ff77a8 14 PINK
-    0xfe75,  // #ffccaa 15 PEACH
+/// @ingroup pw_color
+///
+/// RGB565 values for the Pico-8 color palette.
+constexpr color_rgb565_t kColorsPico8Rgb565[] = {
+    0x0000,  // #000000 0  Black
+    0x194a,  // #1d2b53 1  Dark blue
+    0x792a,  // #7e2553 2  Dark purple
+    0x042a,  // #008751 3  Dark green
+    0xaa86,  // #ab5236 4  Brown
+    0x5aa9,  // #5f574f 5  Dark gray
+    0xc618,  // #c2c3c7 6  Light gray
+    0xff9d,  // #fff1e8 7  White
+    0xf809,  // #ff004d 8  Red
+    0xfd00,  // #ffa300 9  Orange
+    0xff64,  // #ffec27 10 Yellow
+    0x0726,  // #00e436 11 Green
+    0x2d7f,  // #29adff 12 Blue
+    0x83b3,  // #83769c 13 Indigo
+    0xfbb5,  // #ff77a8 14 Pink
+    0xfe75,  // #ffccaa 15 Peach
 };
 
-constexpr color_rgba8888_t colors_pico8_rgba8888[] = {
-    0xff000000,  // #000000 0  BLACK
-    0xff532b1d,  // #1d2b53 1  DARK_BLUE
-    0xff53257e,  // #7e2553 2  DARK_PURPLE
-    0xff518700,  // #008751 3  DARK_GREEN
-    0xff3652ab,  // #ab5236 4  BROWN
-    0xff4f575f,  // #5f574f 5  DARK_GRAY
-    0xffc7c3c2,  // #c2c3c7 6  LIGHT_GRAY
-    0xffe8f1ff,  // #fff1e8 7  WHITE
-    0xff4d00ff,  // #ff004d 8  RED
-    0xff00a3ff,  // #ffa300 9  ORANGE
-    0xff27ecff,  // #ffec27 10 YELLOW
-    0xff36e400,  // #00e436 11 GREEN
-    0xffffad29,  // #29adff 12 BLUE
-    0xff9c7683,  // #83769c 13 INDIGO
-    0xffa877ff,  // #ff77a8 14 PINK
-    0xffaaccff,  // #ffccaa 15 PEACH
+/// @ingroup pw_color
+///
+/// RGB8888 values for the Pico-8 color palette.
+constexpr color_rgba8888_t kColorsPico8Rgba8888[] = {
+    0xff000000,  // #000000 0  Black
+    0xff532b1d,  // #1d2b53 1  Dark blue
+    0xff53257e,  // #7e2553 2  Dark purple
+    0xff518700,  // #008751 3  Dark green
+    0xff3652ab,  // #ab5236 4  Brown
+    0xff4f575f,  // #5f574f 5  Dark gray
+    0xffc7c3c2,  // #c2c3c7 6  Light gray
+    0xffe8f1ff,  // #fff1e8 7  White
+    0xff4d00ff,  // #ff004d 8  Red
+    0xff00a3ff,  // #ffa300 9  Orange
+    0xff27ecff,  // #ffec27 10 Yellow
+    0xff36e400,  // #00e436 11 Green
+    0xffffad29,  // #29adff 12 Blue
+    0xff9c7683,  // #83769c 13 Indigo
+    0xffa877ff,  // #ff77a8 14 Pink
+    0xffaaccff,  // #ffccaa 15 Peach
 };
 
-#define COLOR_BLACK 0
-#define COLOR_DARK_BLUE 1
-#define COLOR_DARK_PURPLE 2
-#define COLOR_DARK_GREEN 3
-#define COLOR_BROWN 4
-#define COLOR_DARK_GRAY 5
-#define COLOR_LIGHT_GRAY 6
-#define COLOR_WHITE 7
-#define COLOR_RED 8
-#define COLOR_ORANGE 9
-#define COLOR_YELLOW 10
-#define COLOR_GREEN 11
-#define COLOR_BLUE 12
-#define COLOR_INDIGO 13
-#define COLOR_PINK 14
-#define COLOR_PEACH 15
+/// @ingroup pw_color
+///
+/// Named color index values.
+enum ColorIndex : uint8_t {
+  // clang-format off
+  kColorBlack      = 0,
+  kColorDarkBlue   = 1,
+  kColorDarkPurple = 2,
+  kColorDarkGreen  = 3,
+  kColorBrown      = 4,
+  kColorDarkGray   = 5,
+  kColorLightGray  = 6,
+  kColorWhite      = 7,
+  kColorRed        = 8,
+  kColorOrange     = 9,
+  kColorYellow     = 10,
+  kColorGreen      = 11,
+  kColorBlue       = 12,
+  kColorIndigo     = 13,
+  kColorPink       = 14,
+  kColorPeach      = 15,
+  // clang-format on
+};
 
 }  // namespace pw::color
diff --git a/pw_graphics/pw_display/BUILD.bazel b/pw_graphics/pw_display/BUILD.bazel
new file mode 100644
index 0000000..71196fa
--- /dev/null
+++ b/pw_graphics/pw_display/BUILD.bazel
@@ -0,0 +1,50 @@
+# Copyright 2024 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.
+
+load(
+    "@pigweed//pw_build:pigweed.bzl",
+    "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "pw_display",
+    srcs = ["display.cc"],
+    hdrs = ["public/pw_display/display.h"],
+    includes = ["public"],
+    deps = [
+        "//pw_display_driver",
+        "//pw_graphics/pw_color",
+        "//pw_graphics/pw_framebuffer",
+        "//pw_graphics/pw_framebuffer_pool",
+        "//pw_graphics/pw_geometry",
+        "@pigweed//pw_assert",
+        "@pigweed//pw_status",
+    ],
+)
+
+pw_cc_test(
+    name = "display_test",
+    srcs = ["display_test.cc"],
+    deps = [":pw_display"],
+)
+
+# Bazel does not yet support building docs.
+filegroup(
+    name = "docs",
+    srcs = ["docs.rst"],
+)
diff --git a/pw_graphics/pw_display/BUILD.gn b/pw_graphics/pw_display/BUILD.gn
index 1d81923..a703419 100644
--- a/pw_graphics/pw_display/BUILD.gn
+++ b/pw_graphics/pw_display/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2024 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
@@ -13,7 +13,10 @@
 # the License.
 
 import("//build_overrides/pigweed.gni")
+
 import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_sync/backend.gni")
 import("$dir_pw_unit_test/test.gni")
 
 declare_args() {
@@ -22,8 +25,9 @@
   pw_display_DISPLAY_RESIZE = "0"
 }
 
-config("public_includes") {
+config("public_include_path") {
   include_dirs = [ "public" ]
+  visibility = [ ":*" ]
 }
 
 config("build_config") {
@@ -31,32 +35,34 @@
   visibility = [ ":*" ]
 }
 
+pw_test_group("tests") {
+  tests = [ ":display_test" ]
+}
+
 pw_source_set("pw_display") {
   public_configs = [
-    ":public_includes",
     ":build_config",
+    ":public_include_path",
   ]
   public = [ "public/pw_display/display.h" ]
-  deps = [ "$dir_pwexperimental_color" ]
+  sources = [ "display.cc" ]
   public_deps = [
     "$dir_pw_assert",
     "$dir_pw_status",
-    "$dir_pwexperimental_display_driver:display_driver",
+    "$dir_pwexperimental_color",
+    "$dir_pwexperimental_display_driver",
     "$dir_pwexperimental_framebuffer",
     "$dir_pwexperimental_framebuffer_pool",
-    "$dir_pwexperimental_math",
+    "$dir_pwexperimental_geometry",
   ]
-  sources = [ "display.cc" ]
 }
 
 pw_test("display_test") {
-  deps = [
-    ":pw_display",
-    "$dir_pwexperimental_color",
-  ]
+  enable_if = pw_sync_COUNTING_SEMAPHORE_BACKEND != ""
   sources = [ "display_test.cc" ]
+  deps = [ ":pw_display" ]
 }
 
-pw_test_group("tests") {
-  tests = [ ":display_test" ]
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
 }
diff --git a/pw_graphics/pw_display/CMakeLists.txt b/pw_graphics/pw_display/CMakeLists.txt
new file mode 100644
index 0000000..24ab8fd
--- /dev/null
+++ b/pw_graphics/pw_display/CMakeLists.txt
@@ -0,0 +1,63 @@
+# Copyright 2024 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_display STATIC
+  SOURCES
+    display.cc
+  HEADERS
+    public/pw_display/display.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_assert
+    pw_color
+    pw_display_driver
+    pw_framebuffer
+    pw_framebuffer_pool
+    pw_geometry
+    pw_status
+)
+
+pw_add_library(pw_display.with_resize STATIC
+  SOURCES
+    display.cc
+  HEADERS
+    public/pw_display/display.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_assert
+    pw_color
+    pw_display_driver
+    pw_framebuffer
+    pw_framebuffer_pool
+    pw_geometry
+    pw_status
+  PUBLIC_DEFINES
+    DISPLAY_RESIZE=1
+)
+
+pw_add_test(pw_display.display_test
+  SOURCES
+    display_test.cc
+  PRIVATE_DEPS
+    pw_display.with_resize
+  GROUPS
+    modules
+    pw_display
+)
+
+# CMake does not yet support building docs.
diff --git a/pw_graphics/pw_display/display.cc b/pw_graphics/pw_display/display.cc
index 317bc93..7f7cc3e 100644
--- a/pw_graphics/pw_display/display.cc
+++ b/pw_graphics/pw_display/display.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -23,12 +23,11 @@
 
 using pw::color::color_rgb565_t;
 using pw::framebuffer::Framebuffer;
-using pw::framebuffer::PixelFormat;
 
 namespace pw::display {
 
 Display::Display(pw::display_driver::DisplayDriver& display_driver,
-                 pw::math::Size<uint16_t> size,
+                 pw::geometry::Size<uint16_t> size,
                  pw::framebuffer_pool::FramebufferPool& framebuffer_pool)
     : display_driver_(display_driver),
       size_(size),
@@ -97,7 +96,8 @@
   pw::framebuffer_pool::FramebufferPool& fb_pool = framebuffer_pool_;
   auto write_cb = [&fb_pool](pw::framebuffer::Framebuffer fb, Status status) {
     PW_ASSERT_OK(status);
-    fb_pool.ReleaseFramebuffer(std::move(fb));
+    Status release_status = fb_pool.ReleaseFramebuffer(std::move(fb));
+    PW_ASSERT_OK(release_status);
   };
   if (!framebuffer.is_valid())
     return Status::InvalidArgument();
diff --git a/pw_graphics/pw_display/display_test.cc b/pw_graphics/pw_display/display_test.cc
index b9f30d8..3d77013 100644
--- a/pw_graphics/pw_display/display_test.cc
+++ b/pw_graphics/pw_display/display_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -25,7 +25,7 @@
 using pw::framebuffer::Framebuffer;
 using pw::framebuffer::PixelFormat;
 using pw::framebuffer_pool::FramebufferPool;
-using Size = pw::math::Size<uint16_t>;
+using Size = pw::geometry::Size<uint16_t>;
 
 namespace pw::display {
 
@@ -55,7 +55,6 @@
 class TestDisplayDriver : public DisplayDriver {
  public:
   TestDisplayDriver(Framebuffer fb) : framebuffer_(std::move(fb)) {}
-  virtual ~TestDisplayDriver() = default;
 
   Status Init() override { return OkStatus(); }
 
@@ -112,7 +111,7 @@
 };
 
 TEST(Display, ReleaseNoResize) {
-  constexpr pw::math::Size<uint16_t> kFramebufferSize{2, 1};
+  constexpr pw::geometry::Size<uint16_t> kFramebufferSize{2, 1};
   constexpr Size kDisplaySize = kFramebufferSize;
   constexpr size_t kNumPixels =
       kFramebufferSize.width * kFramebufferSize.height;
@@ -135,7 +134,7 @@
   EXPECT_EQ(kFramebufferSize, fb.size());
   EXPECT_EQ(0, test_driver.GetNumCalls());
 
-  display.ReleaseFramebuffer(std::move(fb));
+  PW_ASSERT(display.ReleaseFramebuffer(std::move(fb)).ok());
   ASSERT_EQ(1, test_driver.GetNumCalls());
   auto call = test_driver.GetCall(0);
   EXPECT_EQ(CallFunc::ReleaseFramebuffer, call.call_func);
@@ -145,28 +144,29 @@
 #if DISPLAY_RESIZE
 TEST(Display, ReleaseSmallResize) {
   constexpr Size kDisplaySize = {8, 4};
-  constexpr pw::math::Size<uint16_t> kFramebufferSize{2, 1};
+  constexpr pw::geometry::Size<uint16_t> kFramebufferSize{2, 1};
   constexpr size_t kNumPixels =
       kFramebufferSize.width * kFramebufferSize.height;
   constexpr uint16_t kFramebufferRowBytes =
       sizeof(color_rgb565_t) * kFramebufferSize.width;
   color_rgb565_t pixel_data[kNumPixels];
+  pw::Vector<void*, 1> pixel_buffers{pixel_data};
   FramebufferPool fb_pool({
-      .fb_addr = pixel_data,
-      .size = kFramebufferSize,
+      .fb_addr = pixel_buffers,
+      .dimensions = kFramebufferSize,
       .row_bytes = kFramebufferRowBytes,
       .pixel_format = PixelFormat::RGB565,
   });
 
-  TestDisplayDriver test_driver(
-      Framebuffer(pixel_data, kFramebufferSize, kFramebufferRowBytes));
+  TestDisplayDriver test_driver(Framebuffer(
+      pixel_data, PixelFormat::RGB565, kFramebufferSize, kFramebufferRowBytes));
   Display display(test_driver, kDisplaySize, fb_pool);
   Framebuffer fb = display.GetFramebuffer();
   EXPECT_TRUE(fb.is_valid());
   EXPECT_EQ(kFramebufferSize, fb.size());
   EXPECT_EQ(0, test_driver.GetNumCalls());
 
-  display.ReleaseFramebuffer(std::move(fb));
+  PW_ASSERT(display.ReleaseFramebuffer(std::move(fb)).ok());
   ASSERT_EQ(4, test_driver.GetNumCalls());
   auto call = test_driver.GetCall(0);
   EXPECT_EQ(CallFunc::WriteRow, call.call_func);
@@ -196,28 +196,29 @@
 TEST(Display, ReleaseWideResize) {
   // Display width > resize buffer (80 px.) will cause two writes per row.
   constexpr Size kDisplaySize = {90, 4};
-  constexpr pw::math::Size<uint16_t> kFramebufferSize{2, 1};
+  constexpr pw::geometry::Size<uint16_t> kFramebufferSize{2, 1};
   constexpr size_t kNumPixels =
       kFramebufferSize.width * kFramebufferSize.height;
   constexpr uint16_t kFramebufferRowBytes =
       sizeof(color_rgb565_t) * kFramebufferSize.width;
   color_rgb565_t pixel_data[kNumPixels];
+  pw::Vector<void*, 1> pixel_buffers{pixel_data};
   FramebufferPool fb_pool({
-      .fb_addr = pixel_data,
-      .size = kFramebufferSize,
+      .fb_addr = pixel_buffers,
+      .dimensions = kFramebufferSize,
       .row_bytes = kFramebufferRowBytes,
       .pixel_format = PixelFormat::RGB565,
   });
 
-  TestDisplayDriver test_driver(
-      Framebuffer(pixel_data, kFramebufferSize, kFramebufferRowBytes));
-  Display display(test_driver, kDisplaySize);
+  TestDisplayDriver test_driver(Framebuffer(
+      pixel_data, PixelFormat::RGB565, kFramebufferSize, kFramebufferRowBytes));
+  Display display(test_driver, kDisplaySize, fb_pool);
   Framebuffer fb = display.GetFramebuffer();
   EXPECT_TRUE(fb.is_valid());
   EXPECT_EQ(kFramebufferSize, fb.size());
   EXPECT_EQ(0, test_driver.GetNumCalls());
 
-  display.ReleaseFramebuffer(std::move(fb));
+  PW_ASSERT(display.ReleaseFramebuffer(std::move(fb)).ok());
   ASSERT_EQ(8, test_driver.GetNumCalls());
   auto call = test_driver.GetCall(0);
   EXPECT_EQ(CallFunc::WriteRow, call.call_func);
diff --git a/pw_graphics/pw_display/docs.rst b/pw_graphics/pw_display/docs.rst
new file mode 100644
index 0000000..d25dcb0
--- /dev/null
+++ b/pw_graphics/pw_display/docs.rst
@@ -0,0 +1,35 @@
+.. _module-pw_display:
+
+==========
+pw_display
+==========
+.. pigweed-module::
+   :name: pw_display
+
+-------------
+API reference
+-------------
+.. doxygengroup:: pw_display
+   :members:
+
+.. graphics-modules-start
+
+---------------------
+More Graphics Modules
+---------------------
+If this graphics module doesn't provide what you need, check out the others:
+
+* :ref:`module-pw_color`
+* :ref:`module-pw_display_driver_null`
+* :ref:`module-pw_display_driver_st7789`
+* :ref:`module-pw_display_driver`
+* :ref:`module-pw_display`
+* :ref:`module-pw_draw`
+* :ref:`module-pw_framebuffer_pool`
+* :ref:`module-pw_framebuffer`
+* :ref:`module-pw_geometry`
+* :ref:`module-pw_pixel_pusher`
+* :ref:`module-pw_pixel_pusher_rp2040_pio`
+
+.. graphics-modules-end
+
diff --git a/pw_graphics/pw_display/public/pw_display/display.h b/pw_graphics/pw_display/public/pw_display/display.h
index 3fcf5f8..07f277d 100644
--- a/pw_graphics/pw_display/public/pw_display/display.h
+++ b/pw_graphics/pw_display/public/pw_display/display.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -16,57 +16,60 @@
 #include "pw_display_driver/display_driver.h"
 #include "pw_framebuffer/framebuffer.h"
 #include "pw_framebuffer_pool/framebuffer_pool.h"
-#include "pw_math/size.h"
-#include "pw_math/vector3.h"
+#include "pw_geometry/size.h"
 #include "pw_status/status.h"
 
+/// @defgroup pw_display
+
 namespace pw::display {
 
-// The display is an object that represents a single display (or screen)
-// attached to the host. There is a 1:1 correspondence with the screen
-// that it manages. It has one or more framebuffers which its clients may
-// use for rendering.
+/// @ingroup pw_display
+///
+/// The display is an object that represents a single display (or screen)
+/// attached to the host. There is a 1:1 correspondence with the screen
+/// that it manages. It has one or more framebuffers which its clients may
+/// use for rendering.
 class Display {
  public:
   Display(pw::display_driver::DisplayDriver& display_driver,
-          pw::math::Size<uint16_t> size,
+          pw::geometry::Size<uint16_t> size,
           pw::framebuffer_pool::FramebufferPool& framebuffer_pool);
   virtual ~Display();
 
-  // Return a framebuffer to which the caller may draw. When drawing is complete
-  // the framebuffer must be returned using ReleaseFramebuffer(). An invalid
-  // framebuffer may be returned, so the caller should verify it is valid
-  // before use. This function will block until a framebuffer is available for
-  // use. A valid framebuffer will always be returned.
+  /// Return a framebuffer to which the caller may draw. When drawing is
+  /// complete the framebuffer must be returned using ReleaseFramebuffer(). An
+  /// invalid framebuffer may be returned, so the caller should verify it is
+  /// valid before use. This function will block until a framebuffer is
+  /// available for use. A valid framebuffer will always be returned.
   pw::framebuffer::Framebuffer GetFramebuffer();
 
-  // Release the framebuffer back to the display. The display will
-  // send the framebuffer data to the screen. This function will block until
-  // the transfer has completed.
-  //
-  // This function should only be passed a valid framebuffer returned by
-  // a paired call to GetFramebuffer.
-  //
-  // If The pw_display_DISPLAY_RESIZE build variable is set and the display
-  // size is different than the framebuffer size then the framebuffer contents
-  // will be resized using the nearest-neighbor algorithm.
+  /// Release the framebuffer back to the display. The display will
+  /// send the framebuffer data to the screen. This function will block until
+  /// the transfer has completed.
+  ///
+  /// This function should only be passed a valid framebuffer returned by
+  /// a paired call to GetFramebuffer.
+  ///
+  /// If The pw_display_DISPLAY_RESIZE build variable is set and the display
+  /// size is different than the framebuffer size then the framebuffer contents
+  /// will be resized using the nearest-neighbor algorithm.
   Status ReleaseFramebuffer(pw::framebuffer::Framebuffer framebuffer);
 
-  // Return the width (in pixels) of the associated display.
+  /// Return the width (in pixels) of the associated display.
   uint16_t GetWidth() const { return size_.width; }
 
-  // Return the height (in pixels) of the associated display.
+  /// Return the height (in pixels) of the associated display.
   uint16_t GetHeight() const { return size_.height; }
 
  private:
 #if DISPLAY_RESIZE
-  // Update screen while scaling the framebuffer using nearest
-  // neighbor algorithm.
+  /// Update screen while scaling the framebuffer using nearest
+  /// neighbor algorithm.
   Status UpdateNearestNeighbor(const pw::framebuffer::Framebuffer& framebuffer);
 #endif  // if DISPLAY_RESIZE
 
   pw::display_driver::DisplayDriver& display_driver_;
-  const pw::math::Size<uint16_t> size_;
+  const pw::geometry::Size<uint16_t> size_;
   pw::framebuffer_pool::FramebufferPool& framebuffer_pool_;
 };
 
diff --git a/pw_graphics/pw_display_imgui/BUILD.gn b/pw_graphics/pw_display_imgui/BUILD.gn
index f7422b5..ec4e852 100644
--- a/pw_graphics/pw_display_imgui/BUILD.gn
+++ b/pw_graphics/pw_display_imgui/BUILD.gn
@@ -27,7 +27,7 @@
     "$dir_pwexperimental_display",
     "$dir_pwexperimental_display_driver_imgui",
     "$dir_pwexperimental_framebuffer",
-    "$dir_pwexperimental_math",
+    "$dir_pwexperimental_geometry",
   ]
   sources = [ "display.cc" ]
 
diff --git a/pw_graphics/pw_display_imgui/display.cc b/pw_graphics/pw_display_imgui/display.cc
index cf32f9f..04de5fa 100644
--- a/pw_graphics/pw_display_imgui/display.cc
+++ b/pw_graphics/pw_display_imgui/display.cc
@@ -17,7 +17,7 @@
 
 DisplayImgUI::DisplayImgUI(
     pw::display_driver::DisplayDriverImgUI& display_driver,
-    pw::math::Size<uint16_t> size,
+    pw::geometry::Size<uint16_t> size,
     pw::framebuffer_pool::FramebufferPool& framebuffer_pool)
     : Display(display_driver, size, framebuffer_pool),
       display_driver_(display_driver) {}
diff --git a/pw_graphics/pw_display_imgui/public/pw_display_imgui/display.h b/pw_graphics/pw_display_imgui/public/pw_display_imgui/display.h
index fd52ebc..4312acf 100644
--- a/pw_graphics/pw_display_imgui/public/pw_display_imgui/display.h
+++ b/pw_graphics/pw_display_imgui/public/pw_display_imgui/display.h
@@ -15,7 +15,7 @@
 
 #include "pw_display/display.h"
 #include "pw_display_driver_imgui/display_driver.h"
-#include "pw_math/vector3.h"
+#include "pw_geometry/vector3.h"
 #include "pw_status/status.h"
 
 namespace pw::display {
@@ -24,7 +24,7 @@
 class DisplayImgUI : public Display {
  public:
   DisplayImgUI(pw::display_driver::DisplayDriverImgUI& display_driver,
-               pw::math::Size<uint16_t> size,
+               pw::geometry::Size<uint16_t> size,
                pw::framebuffer_pool::FramebufferPool& framebuffer_pool);
   ~DisplayImgUI();
 
diff --git a/pw_graphics/pw_draw/BUILD.bazel b/pw_graphics/pw_draw/BUILD.bazel
new file mode 100644
index 0000000..af0583d
--- /dev/null
+++ b/pw_graphics/pw_draw/BUILD.bazel
@@ -0,0 +1,54 @@
+# Copyright 2024 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.
+
+load(
+    "@pigweed//pw_build:pigweed.bzl",
+    "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "pw_draw",
+    srcs = [
+        "draw.cc",
+        "font6x8.cc",
+        "sprite_sheet.cc",
+        "text_area.cc",
+    ],
+    hdrs = [
+        "public/pw_draw/draw.h",
+        "public/pw_draw/font6x8.h",
+        "public/pw_draw/font_set.h",
+        "public/pw_draw/sprite_sheet.h",
+        "public/pw_draw/text_area.h",
+    ],
+    includes = ["public"],
+    deps = [
+        "//pw_graphics/pw_color",
+        "//pw_graphics/pw_framebuffer",
+        "//pw_graphics/pw_geometry",
+    ],
+)
+
+pw_cc_test(
+    name = "draw_test",
+    srcs = ["draw_test.cc"],
+    deps = [
+        ":pw_draw",
+        "@pigweed//pw_log",
+    ],
+)
diff --git a/pw_graphics/pw_draw/BUILD.gn b/pw_graphics/pw_draw/BUILD.gn
index 9d375e6..75a2e46 100644
--- a/pw_graphics/pw_draw/BUILD.gn
+++ b/pw_graphics/pw_draw/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2024 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
@@ -18,16 +18,20 @@
 import("$dir_pw_docgen/docs.gni")
 import("$dir_pw_unit_test/test.gni")
 
-config("default_config") {
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
+}
+
+config("public_include_path") {
   include_dirs = [ "public" ]
 }
 
 pw_source_set("pw_draw") {
-  public_configs = [ ":default_config" ]
+  public_configs = [ ":public_include_path" ]
   public = [
     "public/pw_draw/draw.h",
+    "public/pw_draw/font6x8.h",
     "public/pw_draw/font_set.h",
-    "public/pw_draw/pigweed_farm.h",
     "public/pw_draw/sprite_sheet.h",
     "public/pw_draw/text_area.h",
   ]
@@ -40,7 +44,7 @@
   public_deps = [
     "$dir_pwexperimental_color",
     "$dir_pwexperimental_framebuffer",
-    "$dir_pwexperimental_math",
+    "$dir_pwexperimental_geometry",
   ]
 }
 
diff --git a/pw_graphics/pw_draw/CMakeLists.txt b/pw_graphics/pw_draw/CMakeLists.txt
new file mode 100644
index 0000000..353884b
--- /dev/null
+++ b/pw_graphics/pw_draw/CMakeLists.txt
@@ -0,0 +1,46 @@
+# Copyright 2024 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_draw STATIC
+  SOURCES
+    draw.cc
+    font6x8.cc
+    sprite_sheet.cc
+    text_area.cc
+  HEADERS
+    public/pw_draw/draw.h
+    public/pw_draw/font6x8.h
+    public/pw_draw/font_set.h
+    public/pw_draw/sprite_sheet.h
+    public/pw_draw/text_area.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_color
+    pw_framebuffer
+    pw_geometry
+)
+
+pw_add_test(pw_draw.draw_test
+  SOURCES
+    draw_test.cc
+  PRIVATE_DEPS
+    pw_log
+    pw_draw
+  GROUPS
+    modules
+    pw_draw
+)
diff --git a/pw_graphics/pw_draw/docs.rst b/pw_graphics/pw_draw/docs.rst
new file mode 100644
index 0000000..1bfa9eb
--- /dev/null
+++ b/pw_graphics/pw_draw/docs.rst
@@ -0,0 +1,28 @@
+.. _module-pw_draw:
+
+==============
+pw_draw
+==============
+.. pigweed-module::
+   :name: pw_draw
+
+.. seealso::
+   This module is part of SEED :ref:`seed-0104`.
+
+``pw_draw`` was created for testing and verification purposes only. It is
+not intended to be feature rich or performant in any way. This is small
+collection of basic drawing primitives not intended to be used by shipping
+applications.
+
+.. important::
+   Currently only ``pw::color::color_rgb565_t`` pixel types are supported.
+
+-------------
+API reference
+-------------
+.. doxygengroup:: pw_draw
+   :members:
+
+.. include:: ../pw_display/docs.rst
+   :start-after: .. graphics-modules-start
+   :end-before: .. graphics-modules-end
diff --git a/pw_graphics/pw_draw/draw.cc b/pw_graphics/pw_draw/draw.cc
index 86cb3b8..316a4e8 100644
--- a/pw_graphics/pw_draw/draw.cc
+++ b/pw_graphics/pw_draw/draw.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -17,6 +17,7 @@
 #include <math.h>
 
 #include "pw_color/color.h"
+#include "pw_draw/font_set.h"
 #include "pw_draw/sprite_sheet.h"
 #include "pw_framebuffer/framebuffer.h"
 #include "pw_framebuffer/writer.h"
@@ -24,29 +25,11 @@
 using pw::color::color_rgb565_t;
 using pw::framebuffer::Framebuffer;
 using pw::framebuffer::FramebufferWriter;
-using pw::math::Size;
-using pw::math::Vector2;
+using pw::geometry::Size;
+using pw::geometry::Vector2;
 
 namespace pw::draw {
 
-namespace {
-
-// Erase a rectangle the size of a font glyph to the background color.
-Size<int> DrawSpace(Vector2<int> pos,
-                    color_rgb565_t bg_color,
-                    const FontSet& font,
-                    Framebuffer& framebuffer) {
-  FramebufferWriter writer(framebuffer);
-  for (int font_row = 0; font_row < font.height; font_row++) {
-    for (int font_column = 0; font_column < font.width; font_column++) {
-      writer.SetPixel(pos.x + font_column, pos.y + font_row, bg_color);
-    }
-  }
-  return Size<int>{font.width, font.height};
-}
-
-}  // namespace
-
 void DrawLine(
     Framebuffer& fb, int x1, int y1, int x2, int y2, color_rgb565_t pen_color) {
   // Bresenham's Line Algorithm
@@ -207,7 +190,7 @@
 }
 
 void DrawTestPattern(Framebuffer& fb) {
-  color_rgb565_t color = pw::color::ColorRGBA(0x00, 0xFF, 0xFF).ToRgb565();
+  color_rgb565_t color = pw::color::ColorRgba(0x00, 0xFF, 0xFF).ToRgb565();
   // Create a Test Pattern
   FramebufferWriter writer(fb);
   for (int x = 0; x < fb.size().width; x++) {
@@ -225,10 +208,6 @@
                         color_rgb565_t bg_color,
                         const FontSet& font,
                         Framebuffer& framebuffer) {
-  if (ch == ' ' || ch == '\0') {
-    // The font doesn't have a space glyph (why?), so special-case this.
-    return DrawSpace(pos, bg_color, font, framebuffer);
-  }
   if (ch < font.starting_character || ch > font.ending_character) {
     return Size<int>{0, font.height};
   }
diff --git a/pw_graphics/pw_draw/draw_test.cc b/pw_graphics/pw_draw/draw_test.cc
index a71348c..e2937fa 100644
--- a/pw_graphics/pw_draw/draw_test.cc
+++ b/pw_graphics/pw_draw/draw_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -17,7 +17,8 @@
 #include "gtest/gtest.h"
 #include "pw_color/color.h"
 #include "pw_color/colors_pico8.h"
-#include "pw_draw/font_set.h"
+#include "pw_draw/draw.h"
+#include "pw_draw/font6x8.h"
 #include "pw_draw/text_area.h"
 #include "pw_framebuffer/framebuffer.h"
 #include "pw_framebuffer/writer.h"
@@ -44,12 +45,12 @@
     for (int x = 0; x < fb.size().width; x++) {
       color_string.clear();
       auto row1_color = reader.GetPixel(x, y);
-      pw::color::ColorRGBA row1(row1_color.ok() ? row1_color.value() : kBlack);
+      pw::color::ColorRgba row1(row1_color.ok() ? row1_color.value() : kBlack);
       auto row2_color = reader.GetPixel(x, y + 1);
       if (!row2_color.ok()) {
         color_string.Format("[38;2;%d;%d;%dm▀", row1.r, row1.g, row1.b);
       } else {
-        pw::color::ColorRGBA row2(row2_color.value());
+        pw::color::ColorRgba row2(row2_color.value());
         color_string.Format("[38;2;%d;%d;%dm[48;2;%d;%d;%dm▀",
                             row1.r,
                             row1.g,
@@ -71,7 +72,7 @@
   color_rgb565_t data[4 * 4];
   Framebuffer fb(data, PixelFormat::RGB565, {4, 4}, 4 * sizeof(data[0]));
   FramebufferWriter writer(fb);
-  color_rgb565_t indigo = color::colors_pico8_rgb565[12];
+  color_rgb565_t indigo = color::kColorsPico8Rgb565[12];
 
   writer.Fill(0);
 
@@ -97,7 +98,7 @@
   color_rgb565_t data[4 * 4];
   Framebuffer fb(data, PixelFormat::RGB565, {4, 4}, 4 * sizeof(data[0]));
   FramebufferWriter writer(fb);
-  color_rgb565_t indigo = color::colors_pico8_rgb565[12];
+  color_rgb565_t indigo = color::kColorsPico8Rgb565[12];
   writer.Fill(0);
 
   // Horizonal line at y = 0
@@ -120,7 +121,7 @@
   color_rgb565_t data[5 * 5];
   Framebuffer fb(data, PixelFormat::RGB565, {5, 5}, 5 * sizeof(data[0]));
   FramebufferWriter writer(fb);
-  color_rgb565_t indigo = color::colors_pico8_rgb565[12];
+  color_rgb565_t indigo = color::kColorsPico8Rgb565[12];
   writer.Fill(0);
 
   // 4x4 rectangle, not filled
@@ -213,7 +214,7 @@
   color_rgb565_t data[5 * 5];
   Framebuffer fb(data, PixelFormat::RGB565, {5, 5}, 5 * sizeof(data[0]));
   FramebufferWriter writer(fb);
-  color_rgb565_t indigo = color::colors_pico8_rgb565[12];
+  color_rgb565_t indigo = color::kColorsPico8Rgb565[12];
   writer.Fill(0);
 
   // 4x4 rectangle, filled
@@ -301,7 +302,7 @@
   color_rgb565_t data[5 * 5];
   Framebuffer fb(data, PixelFormat::RGB565, {5, 5}, 5 * sizeof(data[0]));
   FramebufferWriter writer(fb);
-  color_rgb565_t indigo = color::colors_pico8_rgb565[12];
+  color_rgb565_t indigo = color::kColorsPico8Rgb565[12];
   writer.Fill(0);
 
   // 4x4 rectangle, not filled
@@ -394,7 +395,7 @@
   color_rgb565_t data[7 * 7];
   Framebuffer fb(data, PixelFormat::RGB565, {7, 7}, 7 * sizeof(data[0]));
   FramebufferWriter writer(fb);
-  color_rgb565_t indigo = color::colors_pico8_rgb565[12];
+  color_rgb565_t indigo = color::kColorsPico8Rgb565[12];
   writer.Fill(0);
 
   DrawCircle(fb, 3, 3, 3, indigo, false);
@@ -572,10 +573,10 @@
   FramebufferWriter writer(fb);
   writer.Fill(0);
 
-  pw::draw::TextArea text_area(fb, &font6x8);
-  text_area.SetForegroundColor(color::colors_pico8_rgb565[COLOR_PINK]);
+  pw::draw::TextArea text_area(fb, GetFont6x8());
+  text_area.SetForegroundColor(color::kColorsPico8Rgb565[color::kColorPink]);
   text_area.SetBackgroundColor(0);
-  text_area.DrawText("Hell\noT\nhere.\nWorld");
+  text_area.DrawText("Hi There.\nWorld");
 
   PrintFramebufferAsANSI(fb);
 }
diff --git a/pw_graphics/pw_draw/font6x8.cc b/pw_graphics/pw_draw/font6x8.cc
index 661bb08..741d4bc 100644
--- a/pw_graphics/pw_draw/font6x8.cc
+++ b/pw_graphics/pw_draw/font6x8.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -14,6 +14,7 @@
 
 #include "pw_draw/font_set.h"
 
+namespace {
 static const uint8_t font6x8_data[] = {
     // 32 Space
     0b000000,
@@ -1298,9 +1299,23 @@
     0b111111,
 };
 
+const pw::draw::FontSet kFont6x8BaseSet(font6x8_data,
+                                        /*character_pixel_width=*/6,
+                                        /*character_pixel_height=*/8,
+                                        /*start_char=*/32,
+                                        /*end_char=*/127);
+const pw::draw::FontSet kFont6x8BoxSet(font6x8_box_chars_data,
+                                       /*character_pixel_width=*/6,
+                                       /*character_pixel_height=*/8,
+                                       /*start_char=*/0x2580,
+                                       /*end_char=*/0x259A);
+
+}  // namespace
+
 namespace pw::draw {
 
-const FontSet font6x8_box_chars(font6x8_box_chars_data, 6, 8, 0x2580, 0x259A);
-const FontSet font6x8(font6x8_data, 6, 8, 32, 127);
+const FontSet& GetFont6x8() { return kFont6x8BaseSet; }
+
+const FontSet& GetFont6x8BoxChars() { return kFont6x8BoxSet; }
 
 }  // namespace pw::draw
diff --git a/pw_graphics/pw_draw/public/pw_draw/draw.h b/pw_graphics/pw_draw/public/pw_draw/draw.h
index 35c38db..c32d8ae 100644
--- a/pw_graphics/pw_draw/public/pw_draw/draw.h
+++ b/pw_graphics/pw_draw/public/pw_draw/draw.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -21,11 +21,17 @@
 #include "pw_draw/font_set.h"
 #include "pw_draw/sprite_sheet.h"
 #include "pw_framebuffer/framebuffer.h"
-#include "pw_math/size.h"
-#include "pw_math/vector2.h"
+#include "pw_geometry/size.h"
+#include "pw_geometry/vector2.h"
+
+/// @defgroup pw_draw
 
 namespace pw::draw {
 
+/// @ingroup pw_draw
+///
+/// Draw a line using Bresenham's line algorithm from point (x1, y1) to (x2,
+/// y2).
 void DrawLine(pw::framebuffer::Framebuffer& fb,
               int x1,
               int y1,
@@ -33,8 +39,10 @@
               int y2,
               pw::color::color_rgb565_t pen_color);
 
-// Draw a circle at center_x, center_y with given radius and color. Only a
-// one-pixel outline is drawn if filled is false.
+/// @ingroup pw_draw
+///
+/// Draw a circle at (center_x, center_y) with given radius and color. Only a
+/// one-pixel outline is drawn if filled is false.
 void DrawCircle(pw::framebuffer::Framebuffer& fb,
                 int center_x,
                 int center_y,
@@ -42,12 +50,19 @@
                 pw::color::color_rgb565_t pen_color,
                 bool filled);
 
+/// @ingroup pw_draw
+///
+/// Draw a horizontal line from (x1, y) to (x2, y).
 void DrawHLine(pw::framebuffer::Framebuffer& fb,
                int x1,
                int x2,
                int y,
                pw::color::color_rgb565_t pen_color);
 
+/// @ingroup pw_draw
+///
+/// Draw a rectangle defined by two opposite corner points (x1, y1) and (x2,
+/// y2).
 void DrawRect(pw::framebuffer::Framebuffer& fb,
               int x1,
               int y1,
@@ -56,6 +71,10 @@
               pw::color::color_rgb565_t pen_color,
               bool filled);
 
+/// @ingroup pw_draw
+///
+/// Draw a rectangle defined by an upper left point (x, y) and width height
+/// parameters.
 void DrawRectWH(pw::framebuffer::Framebuffer& fb,
                 int x,
                 int y,
@@ -64,9 +83,15 @@
                 pw::color::color_rgb565_t pen_color,
                 bool filled);
 
+/// @ingroup pw_draw
+///
+/// Fill an entire framebuffer with a single color.
 void Fill(pw::framebuffer::Framebuffer& fb,
           pw::color::color_rgb565_t pen_color);
 
+/// @ingroup pw_draw
+///
+/// Draw a sprite at positon (x, y).
 void DrawSprite(pw::framebuffer::Framebuffer& fb,
                 int x,
                 int y,
@@ -75,18 +100,26 @@
 
 void DrawTestPattern();
 
-pw::math::Size<int> DrawCharacter(int ch,
-                                  pw::math::Vector2<int> pos,
-                                  pw::color::color_rgb565_t fg_color,
-                                  pw::color::color_rgb565_t bg_color,
-                                  const FontSet& font,
-                                  pw::framebuffer::Framebuffer& framebuffer);
+/// @ingroup pw_draw
+///
+/// Draw a single character with a given foreground and background color.
+pw::geometry::Size<int> DrawCharacter(
+    int ch,
+    pw::geometry::Vector2<int> pos,
+    pw::color::color_rgb565_t fg_color,
+    pw::color::color_rgb565_t bg_color,
+    const FontSet& font,
+    pw::framebuffer::Framebuffer& framebuffer);
 
-pw::math::Size<int> DrawString(std::wstring_view str,
-                               pw::math::Vector2<int> pos,
-                               pw::color::color_rgb565_t fg_color,
-                               pw::color::color_rgb565_t bg_color,
-                               const FontSet& font,
-                               pw::framebuffer::Framebuffer& framebuffer);
+/// @ingroup pw_draw
+///
+/// Draw a string of characters onto a framebuffer. No text wrapping or line
+/// break characters are handled.
+pw::geometry::Size<int> DrawString(std::wstring_view str,
+                                   pw::geometry::Vector2<int> pos,
+                                   pw::color::color_rgb565_t fg_color,
+                                   pw::color::color_rgb565_t bg_color,
+                                   const FontSet& font,
+                                   pw::framebuffer::Framebuffer& framebuffer);
 
 }  // namespace pw::draw
diff --git a/pw_graphics/pw_draw/public/pw_draw/font6x8.h b/pw_graphics/pw_draw/public/pw_draw/font6x8.h
new file mode 100644
index 0000000..83c5590
--- /dev/null
+++ b/pw_graphics/pw_draw/public/pw_draw/font6x8.h
@@ -0,0 +1,32 @@
+// Copyright 2024 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.
+#pragma once
+
+#include "pw_draw/font_set.h"
+
+namespace pw::draw {
+
+/// @ingroup pw_draw
+///
+/// Return a FontSet for a simple 6x8 pixel font. ASCII characters 32-126 are
+/// included.
+const FontSet& GetFont6x8();
+
+/// @ingroup pw_draw
+///
+/// Return a FontSet for various box drawing characters. Includes unicode
+/// characters 0x2580 through 0x259A.
+const FontSet& GetFont6x8BoxChars();
+
+}  // namespace pw::draw
diff --git a/pw_graphics/pw_draw/public/pw_draw/font_set.h b/pw_graphics/pw_draw/public/pw_draw/font_set.h
index 3610147..7c066de 100644
--- a/pw_graphics/pw_draw/public/pw_draw/font_set.h
+++ b/pw_graphics/pw_draw/public/pw_draw/font_set.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -20,12 +20,19 @@
 // Grab the x'th bit from a number
 #define PW_FONT_BIT(x, number) (((number) >> (x)) & 1);
 
+/// @ingroup pw_draw
+///
+/// Container struct representing a set of characters.
 struct FontSet {
-  constexpr FontSet(
-      const uint8_t* d, uint8_t w, uint8_t h, int start_char, int end_char)
-      : data(d),
-        width(w),
-        height(h),
+  /// Create a new FontSet.
+  constexpr FontSet(const uint8_t* pixel_data,
+                    uint8_t character_pixel_width,
+                    uint8_t character_pixel_height,
+                    int start_char,
+                    int end_char)
+      : data(pixel_data),
+        width(character_pixel_width),
+        height(character_pixel_height),
         starting_character(start_char),
         ending_character(end_char) {}
 
@@ -36,7 +43,4 @@
   const int ending_character;
 };
 
-extern const FontSet font6x8;
-extern const FontSet font6x8_box_chars;
-
 }  // namespace pw::draw
diff --git a/pw_graphics/pw_draw/public/pw_draw/sprite_sheet.h b/pw_graphics/pw_draw/public/pw_draw/sprite_sheet.h
index d3a03b8..bcc380e 100644
--- a/pw_graphics/pw_draw/public/pw_draw/sprite_sheet.h
+++ b/pw_graphics/pw_draw/public/pw_draw/sprite_sheet.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -18,6 +18,9 @@
 
 namespace pw::draw {
 
+/// @ingroup pw_draw
+///
+/// Container class representing a set of sprites.
 class SpriteSheet {
  public:
   const int width;
diff --git a/pw_graphics/pw_draw/public/pw_draw/text_area.h b/pw_graphics/pw_draw/public/pw_draw/text_area.h
index db25d0c..45420f7 100644
--- a/pw_graphics/pw_draw/public/pw_draw/text_area.h
+++ b/pw_graphics/pw_draw/public/pw_draw/text_area.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -19,43 +19,58 @@
 
 namespace pw::draw {
 
+/// @ingroup pw_draw
+///
+/// Class for drawing text to an area of a Framebuffer. Handles newlines and
+/// wrapping to the next line on each character.
 class TextArea {
  public:
-  int cursor_x;
-  int cursor_y;
-  int column_count;
-  bool character_wrap_enabled;
-  const FontSet* current_font;
-  pw::color::color_rgb565_t foreground_color;
-  pw::color::color_rgb565_t background_color;
-  pw::framebuffer::Framebuffer& framebuffer;
+  /// Initialize a TextArea with a Framebuffer and FontSet.
+  TextArea(pw::framebuffer::Framebuffer& fb, const FontSet& font);
 
-  TextArea(pw::framebuffer::Framebuffer& fb, const FontSet* font);
-
-  // Change the current font.
-  void SetFont(const FontSet* new_font);
+  /// Enable or disable character wrapping.
   void SetCharacterWrap(bool new_setting);
+  /// Set the cursor to position (x, y) then draw a single character.
   void SetCursor(int x, int y);
+  /// Shift all lines in the text area up by a given character line count.
   void ScrollUp(int lines);
 
+  /// Draw a single character at the current cursor position.
   void DrawCharacter(int character);
+  /// Set the cursor to position (x, y) then draw a single character.
   void DrawCharacter(int character, int x, int y);
 
+  /// Set the text foreground color.
   void SetForegroundColor(pw::color::color_rgb565_t color);
+  /// Set the text background color.
   void SetBackgroundColor(pw::color::color_rgb565_t color);
 
-  void DrawTestFontSheet(int character_width, int x, int y);
+  /// Draw all characters in the current FontSet at position (x, y).
+  void DrawTestFontSheet(int character_column_width, int x, int y);
 
-  // DrawText at x, y (upper left pixel of font). Carriage returns will move
-  // text to the next line.
+  /// Draw a string at the current cursor position.
   void DrawText(const char* str);
+  /// Set the cursor to position (x, y) then draw a string.
   void DrawText(const char* str, int x, int y);
-
+  /// Draw a string at the current cursor position.
   void DrawText(const wchar_t* str);
+  /// Set the cursor to position (x, y) then draw a string.
   void DrawText(const wchar_t* str, int x, int y);
 
+  /// Move the cursor forward one character (to the right).
   void MoveCursorRightOnce();
+  /// Move the cursor down to the beginning of the next line.
   void InsertLineBreak();
+
+ private:
+  int cursor_x_;
+  int cursor_y_;
+  int column_count_;
+  bool character_wrap_enabled_;
+  const FontSet& font_;
+  pw::color::color_rgb565_t foreground_color_;
+  pw::color::color_rgb565_t background_color_;
+  pw::framebuffer::Framebuffer& framebuffer_;
 };
 
 }  // namespace pw::draw
diff --git a/pw_graphics/pw_draw/sprite_sheet.cc b/pw_graphics/pw_draw/sprite_sheet.cc
index d482e97..c2fb758 100644
--- a/pw_graphics/pw_draw/sprite_sheet.cc
+++ b/pw_graphics/pw_draw/sprite_sheet.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
diff --git a/pw_graphics/pw_draw/text_area.cc b/pw_graphics/pw_draw/text_area.cc
index eac3b95..d6f4901 100644
--- a/pw_graphics/pw_draw/text_area.cc
+++ b/pw_graphics/pw_draw/text_area.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -22,54 +22,51 @@
 
 using pw::color::color_rgb565_t;
 using pw::framebuffer::FramebufferWriter;
-using pw::math::Vector2;
+using pw::geometry::Vector2;
 
 namespace pw::draw {
 
-TextArea::TextArea(pw::framebuffer::Framebuffer& fb, const FontSet* font)
-    : framebuffer(fb) {
-  SetFont(font);
+TextArea::TextArea(pw::framebuffer::Framebuffer& fb, const FontSet& font)
+    : font_(font), framebuffer_(fb) {
+  // SetFont(font);
   // Default colors: White on Black
-  character_wrap_enabled = true;
-  foreground_color = 0xFFFF;
-  background_color = 0;
+  character_wrap_enabled_ = true;
+  foreground_color_ = 0xFFFF;
+  background_color_ = 0;
   SetCursor(0, 0);
 }
 
-// Change the current font.
-void TextArea::SetFont(const FontSet* new_font) { current_font = new_font; }
-
 void TextArea::SetCursor(int x, int y) {
-  cursor_x = x;
-  cursor_y = y;
-  column_count = 0;
+  cursor_x_ = x;
+  cursor_y_ = y;
+  column_count_ = 0;
 }
 
 void TextArea::SetForegroundColor(color_rgb565_t color) {
-  foreground_color = color;
+  foreground_color_ = color;
 }
 
 void TextArea::SetBackgroundColor(color_rgb565_t color) {
-  background_color = color;
+  background_color_ = color;
 }
 
 void TextArea::SetCharacterWrap(bool new_setting) {
-  character_wrap_enabled = new_setting;
+  character_wrap_enabled_ = new_setting;
 }
 
 void TextArea::MoveCursorRightOnce() {
-  cursor_x = cursor_x + current_font->width;
-  column_count++;
+  cursor_x_ = cursor_x_ + font_.width;
+  column_count_++;
 }
 
 void TextArea::InsertLineBreak() {
-  cursor_y = cursor_y + current_font->height;
-  cursor_x = cursor_x - (column_count * current_font->width);
-  column_count = 0;
+  cursor_y_ = cursor_y_ + font_.height;
+  cursor_x_ = cursor_x_ - (column_count_ * font_.width);
+  column_count_ = 0;
 
-  if (cursor_y >= framebuffer.size().height) {
+  if (cursor_y_ >= framebuffer_.size().height) {
     ScrollUp(1);
-    cursor_y = cursor_y - current_font->height;
+    cursor_y_ = cursor_y_ - font_.height;
   }
 }
 
@@ -79,24 +76,24 @@
     return;
   }
 
-  if ((int)character < current_font->starting_character ||
-      (int)character > current_font->ending_character) {
+  if ((int)character < font_.starting_character ||
+      (int)character > font_.ending_character) {
     // Unprintable character
     MoveCursorRightOnce();
     return;
   }
 
-  if (character_wrap_enabled &&
-      (current_font->width + cursor_x) > framebuffer.size().width) {
+  if (character_wrap_enabled_ &&
+      (font_.width + cursor_x_) > framebuffer_.size().width) {
     InsertLineBreak();
   }
 
   pw::draw::DrawCharacter(character,
-                          Vector2<int>{cursor_x, cursor_y},
-                          foreground_color,
-                          background_color,
-                          *current_font,
-                          framebuffer);
+                          Vector2<int>{cursor_x_, cursor_y_},
+                          foreground_color_,
+                          background_color_,
+                          font_,
+                          framebuffer_);
 
   // Move cursor to the right by 1 glyph.
   MoveCursorRightOnce();
@@ -109,10 +106,8 @@
 
 void TextArea::DrawTestFontSheet(int character_column_width, int x, int y) {
   SetCursor(x, y);
-  for (int c = current_font->starting_character;
-       c <= current_font->ending_character;
-       c++) {
-    int index = c - current_font->starting_character;
+  for (int c = font_.starting_character; c <= font_.ending_character; c++) {
+    int index = c - font_.starting_character;
     if (index > 0 && index % character_column_width == 0) {
       DrawCharacter('\n');
     }
@@ -145,28 +140,30 @@
 }
 
 void TextArea::ScrollUp(int lines) {
-  int pixel_height = lines * current_font->height;
+  int pixel_height = lines * font_.height;
   int start_x = 0;
   int start_y = pixel_height;
 
-  FramebufferWriter writer(framebuffer);
-  for (int current_x = 0; current_x < framebuffer.size().width; current_x++) {
-    for (int current_y = start_y; current_y < framebuffer.size().height;
-         current_y++) {
-      if (auto pixel_color = writer.GetPixel(current_x, current_y);
+  FramebufferWriter writer(framebuffer_);
+  for (int current_x_ = 0; current_x_ < framebuffer_.size().width;
+       current_x_++) {
+    for (int current_y_ = start_y; current_y_ < framebuffer_.size().height;
+         current_y_++) {
+      if (auto pixel_color = writer.GetPixel(current_x_, current_y_);
           pixel_color.ok()) {
-        writer.SetPixel(start_x + current_x, current_y - start_y, *pixel_color);
+        writer.SetPixel(
+            start_x + current_x_, current_y_ - start_y, *pixel_color);
       }
     }
   }
 
   // Draw a filled background_color rectangle at the bottom to erase the old
   // text.
-  for (int x = 0; x < framebuffer.size().width; x++) {
-    for (int y = framebuffer.size().height - pixel_height;
-         y < framebuffer.size().height;
+  for (int x = 0; x < framebuffer_.size().width; x++) {
+    for (int y = framebuffer_.size().height - pixel_height;
+         y < framebuffer_.size().height;
          y++) {
-      writer.SetPixel(x, y, background_color);
+      writer.SetPixel(x, y, background_color_);
     }
   }
 }
diff --git a/pw_graphics/pw_framebuffer/BUILD.bazel b/pw_graphics/pw_framebuffer/BUILD.bazel
new file mode 100644
index 0000000..8b66636
--- /dev/null
+++ b/pw_graphics/pw_framebuffer/BUILD.bazel
@@ -0,0 +1,67 @@
+# Copyright 2024 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.
+
+load(
+    "@pigweed//pw_build:pigweed.bzl",
+    "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "pw_framebuffer",
+    srcs = [
+        "framebuffer.cc",
+        "reader.cc",
+        "writer.cc",
+    ],
+    hdrs = [
+        "public/pw_framebuffer/framebuffer.h",
+        "public/pw_framebuffer/reader.h",
+        "public/pw_framebuffer/writer.h",
+    ],
+    includes = ["public"],
+    deps = [
+        "//pw_graphics/pw_color",
+        "//pw_graphics/pw_geometry",
+        "@pigweed//pw_assert",
+        "@pigweed//pw_result",
+    ],
+)
+
+pw_cc_test(
+    name = "framebuffer_test",
+    srcs = ["framebuffer_test.cc"],
+    deps = [
+        ":pw_framebuffer",
+    ],
+)
+
+pw_cc_test(
+    name = "reader_test",
+    srcs = ["reader_test.cc"],
+    deps = [
+        ":pw_framebuffer",
+    ],
+)
+
+pw_cc_test(
+    name = "writer_test",
+    srcs = ["writer_test.cc"],
+    deps = [
+        ":pw_framebuffer",
+    ],
+)
diff --git a/pw_graphics/pw_framebuffer/BUILD.gn b/pw_graphics/pw_framebuffer/BUILD.gn
index 6f1616b..fd76deb 100644
--- a/pw_graphics/pw_framebuffer/BUILD.gn
+++ b/pw_graphics/pw_framebuffer/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2024 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
@@ -18,6 +18,10 @@
 import("$dir_pw_docgen/docs.gni")
 import("$dir_pw_unit_test/test.gni")
 
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
+}
+
 config("default_config") {
   include_dirs = [ "public" ]
 }
@@ -28,7 +32,7 @@
   public_deps = [
     "$dir_pw_result",
     "$dir_pwexperimental_color",
-    "$dir_pwexperimental_math",
+    "$dir_pwexperimental_geometry",
   ]
   public = [
     "public/pw_framebuffer/framebuffer.h",
diff --git a/pw_graphics/pw_framebuffer/CMakeLists.txt b/pw_graphics/pw_framebuffer/CMakeLists.txt
new file mode 100644
index 0000000..3a036c5
--- /dev/null
+++ b/pw_graphics/pw_framebuffer/CMakeLists.txt
@@ -0,0 +1,63 @@
+# Copyright 2024 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_framebuffer STATIC
+  SOURCES
+    framebuffer.cc
+    reader.cc
+    writer.cc
+  HEADERS
+    public/pw_framebuffer/framebuffer.h
+    public/pw_framebuffer/reader.h
+    public/pw_framebuffer/writer.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_assert
+    pw_color
+    pw_geometry
+    pw_result
+)
+
+pw_add_test(pw_framebuffer.framebuffer_test
+  SOURCES
+    framebuffer_test.cc
+  PRIVATE_DEPS
+    pw_framebuffer
+  GROUPS
+    modules
+    pw_framebuffer
+)
+
+pw_add_test(pw_framebuffer.reader_test
+  SOURCES
+    reader_test.cc
+  PRIVATE_DEPS
+    pw_framebuffer
+  GROUPS
+    modules
+    pw_framebuffer
+)
+
+pw_add_test(pw_framebuffer.writer_test
+  SOURCES
+    writer_test.cc
+  PRIVATE_DEPS
+    pw_framebuffer
+  GROUPS
+    modules
+    pw_framebuffer
+)
diff --git a/pw_graphics/pw_framebuffer/docs.rst b/pw_graphics/pw_framebuffer/docs.rst
new file mode 100644
index 0000000..2d2811a
--- /dev/null
+++ b/pw_graphics/pw_framebuffer/docs.rst
@@ -0,0 +1,43 @@
+.. _module-pw_framebuffer:
+
+==============
+pw_framebuffer
+==============
+.. pigweed-module::
+   :name: pw_framebuffer
+
+.. seealso::
+   This module is part of SEED :ref:`seed-0104`.
+
+   Other Pigweed graphics modules:
+   :bdg-ref-primary-line:`module-pw_color`
+   :bdg-ref-primary-line:`module-pw_geometry`
+   :bdg-ref-primary-line:`module-pw_framebuffer`
+   :bdg-ref-primary-line:`module-pw_draw`
+
+.. cpp:namespace-push:: pw::framebuffer
+
+:cpp:class:`Framebuffer` is a small class that provides access
+to a pixel buffer. It keeps a copy of the pixel buffer metadata and provides
+accessor methods for those values.
+
+:cpp:class:`Framebuffer` is a moveable class that is intended
+to signify read/write privileges to the underlying pixel data. This makes it
+easier to track when the pixel data may be read from, or written to, without
+conflict.
+
+The Framebuffer does not own the underlying pixel buffer. In other words the
+deletion of a framebuffer will not free the underlying pixel data.
+
+Framebuffers do not have methods for reading or writing to the underlying pixel
+buffer. This is the responsibility of the the selected graphics library which
+can be given the pixel buffer pointer retrieved by calling
+:cpp:func:`Framebuffer::data()`.
+
+.. cpp:namespace-pop::
+
+-------------
+API reference
+-------------
+.. doxygengroup:: pw_framebuffer
+   :members:
diff --git a/pw_graphics/pw_framebuffer/framebuffer.cc b/pw_graphics/pw_framebuffer/framebuffer.cc
index 531b685..8a5ff43 100644
--- a/pw_graphics/pw_framebuffer/framebuffer.cc
+++ b/pw_graphics/pw_framebuffer/framebuffer.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -27,7 +27,7 @@
 
 Framebuffer::Framebuffer(void* data,
                          PixelFormat pixel_format,
-                         pw::math::Size<uint16_t> size,
+                         pw::geometry::Size<uint16_t> size,
                          uint16_t row_bytes)
     : pixel_data_(data),
       pixel_format_(pixel_format),
diff --git a/pw_graphics/pw_framebuffer/framebuffer_test.cc b/pw_graphics/pw_framebuffer/framebuffer_test.cc
index 1ba589a..8ddb8ad 100644
--- a/pw_graphics/pw_framebuffer/framebuffer_test.cc
+++ b/pw_graphics/pw_framebuffer/framebuffer_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -18,11 +18,9 @@
 
 #include "gtest/gtest.h"
 #include "pw_color/color.h"
-#include "pw_color/colors_endesga32.h"
 #include "pw_color/colors_pico8.h"
 #include "pw_framebuffer/writer.h"
-#include "pw_log/log.h"
-#include "pw_math/size.h"
+#include "pw_geometry/size.h"
 
 using pw::color::color_rgb565_t;
 
@@ -48,7 +46,7 @@
 }
 
 TEST(Framebuffer, Init) {
-  constexpr pw::math::Size<uint16_t> kDimensions = {32, 40};
+  constexpr pw::geometry::Size<uint16_t> kDimensions = {32, 40};
   constexpr uint16_t kRowBytes = kDimensions.width * sizeof(color_rgb565_t);
 
   color_rgb565_t data[kDimensions.width * kDimensions.height];
diff --git a/pw_graphics/pw_framebuffer/public/pw_framebuffer/framebuffer.h b/pw_graphics/pw_framebuffer/public/pw_framebuffer/framebuffer.h
index ac9820b..9ff8526 100644
--- a/pw_graphics/pw_framebuffer/public/pw_framebuffer/framebuffer.h
+++ b/pw_graphics/pw_framebuffer/public/pw_framebuffer/framebuffer.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -15,27 +15,34 @@
 
 #include <cstdint>
 
-#include "pw_math/size.h"
+#include "pw_geometry/size.h"
+
+/// @defgroup pw_framebuffer
 
 namespace pw::framebuffer {
 
+/// @ingroup pw_framebuffer
+///
+/// Enum for framebuffer pixel format.
 enum class PixelFormat {
   None,
   RGB565,
 };
 
-// A Framebuffer refers to a buffer of pixel data and the various attributes
-// of that pixel data (such as dimensions, rowbytes, etc.).
+/// @ingroup pw_framebuffer
+///
+/// A Framebuffer refers to a buffer of pixel data and the various attributes of
+/// that pixel data (such as dimensions, rowbytes, etc.).
 class Framebuffer {
  public:
-  // Construct a default invalid framebuffer.
+  /// Construct a default invalid framebuffer.
   Framebuffer();
 
-  // Construct a framebuffer of the specified dimensions which *does not* own
-  // the |data| - i.e. this instance will never attempt to free it.
+  /// Construct a framebuffer of the specified dimensions which *does not* own
+  /// the |data| - i.e. this instance will never attempt to free it.
   Framebuffer(void* data,
               PixelFormat pixel_format,
-              pw::math::Size<uint16_t> size,
+              pw::geometry::Size<uint16_t> size,
               uint16_t row_bytes);
 
   Framebuffer(const Framebuffer&) = delete;
@@ -44,27 +51,31 @@
   Framebuffer& operator=(const Framebuffer&) = delete;
   Framebuffer& operator=(Framebuffer&&);
 
-  // Has the framebuffer been properly initialized?
-  bool is_valid() const { return pixel_data_ != nullptr; };
+  /// Has the framebuffer been properly initialized?
+  bool is_valid() const { return pixel_data_ != nullptr; }
 
-  // Return a pointer to the framebuffer pixel buffer.
+  /// Return a pointer to the framebuffer pixel buffer.
   void* data() const { return pixel_data_; }
 
-  // Return the format of all pixels managed by this framebuffer.
+  /// Return the format of all pixels managed by this framebuffer.
   PixelFormat pixel_format() const { return pixel_format_; }
 
-  // Return the framebuffer size which is the width and height of the
-  // framebuffer in pixels.
-  pw::math::Size<uint16_t> size() const { return size_; }
+  /// Return the framebuffer size which is the width and height of the
+  /// framebuffer in pixels.
+  pw::geometry::Size<uint16_t> size() const { return size_; }
 
-  // Return the number of bytes per row of pixel data.
+  /// Return the number of bytes per row of pixel data.
   uint16_t row_bytes() const { return row_bytes_; }
 
  private:
-  void* pixel_data_;               // The pixel buffer.
-  PixelFormat pixel_format_;       // The pixel format.
-  pw::math::Size<uint16_t> size_;  // width/height (in pixels) of |pixel_data_|.
-  uint16_t row_bytes_;             // The number of bytes in each row.
+  /// The pixel buffer.
+  void* pixel_data_;
+  /// The pixel format.
+  PixelFormat pixel_format_;
+  /// width/height (in pixels) of |pixel_data_|.
+  pw::geometry::Size<uint16_t> size_;
+  /// The number of bytes in each row.
+  uint16_t row_bytes_;
 };
 
 }  // namespace pw::framebuffer
diff --git a/pw_graphics/pw_framebuffer/public/pw_framebuffer/reader.h b/pw_graphics/pw_framebuffer/public/pw_framebuffer/reader.h
index f9c2df5..3a87e7d 100644
--- a/pw_graphics/pw_framebuffer/public/pw_framebuffer/reader.h
+++ b/pw_graphics/pw_framebuffer/public/pw_framebuffer/reader.h
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -21,17 +21,19 @@
 
 namespace pw::framebuffer {
 
-// An interface to Framebuffer to simplify reading pixel values from a
-// framebuffer.
-//
-// Note: This implementation is not designed for performance, and is intended
-// to be used for development (testing) and other cases where drawing
-// performance is not important.
+/// @ingroup pw_framebuffer
+///
+/// An interface to Framebuffer to simplify reading pixel values from a
+/// framebuffer.
+///
+/// Note: This implementation is not designed for performance, and is intended
+/// to be used for development (testing) and other cases where drawing
+/// performance is not important.
 class FramebufferReader {
  public:
   FramebufferReader(const Framebuffer& framebuffer);
 
-  // Return the pixel value at position (x, y). Bounds are checked.
+  /// Return the pixel value at position (x, y). Bounds are checked.
   Result<pw::color::color_rgb565_t> GetPixel(uint16_t x, uint16_t y) const;
 
  protected:
diff --git a/pw_graphics/pw_framebuffer/public/pw_framebuffer/writer.h b/pw_graphics/pw_framebuffer/public/pw_framebuffer/writer.h
index 8f2851c..9208dfe 100644
--- a/pw_graphics/pw_framebuffer/public/pw_framebuffer/writer.h
+++ b/pw_graphics/pw_framebuffer/public/pw_framebuffer/writer.h
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -21,25 +21,27 @@
 
 namespace pw::framebuffer {
 
-// An interface to Framebuffer to simplify writing (and reading) pixel values
-// from a framebuffer.
-//
-// Note: This implementation is not designed for performance, and is intended
-// to be used for development (testing) and other cases where drawing
-// performance is not important.
+/// @ingroup pw_framebuffer
+///
+/// An interface to Framebuffer to simplify writing (and reading) pixel values
+/// from a framebuffer.
+///
+/// Note: This implementation is not designed for performance, and is intended
+/// to be used for development (testing) and other cases where drawing
+/// performance is not important.
 class FramebufferWriter : public FramebufferReader {
  public:
   FramebufferWriter(Framebuffer& framebuffer);
 
-  // Set the pixel at (x, y), if within the framebuffer bounds, to the
-  // specified pixel value.
+  /// Set the pixel at (x, y), if within the framebuffer bounds, to the
+  /// specified pixel value.
   void SetPixel(uint16_t x, uint16_t y, pw::color::color_rgb565_t pixel_value);
 
-  // Copy the pixels from another framebuffer into the one managed by this
-  // writer at position (x, y).
+  /// Copy the pixels from another framebuffer into the one managed by this
+  /// writer at position (x, y).
   void Blit(const Framebuffer& fb, uint16_t x, uint16_t y);
 
-  // Fill the entire framebuffer with the specified pixel value.
+  /// Fill the entire framebuffer with the specified pixel value.
   void Fill(pw::color::color_rgb565_t pixel_value);
 };
 
diff --git a/pw_graphics/pw_framebuffer/reader.cc b/pw_graphics/pw_framebuffer/reader.cc
index 7f976f7..d0e7962 100644
--- a/pw_graphics/pw_framebuffer/reader.cc
+++ b/pw_graphics/pw_framebuffer/reader.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -11,8 +11,8 @@
 // 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 "public/pw_framebuffer/writer.h"
 #include "pw_assert/assert.h"
+#include "pw_framebuffer/writer.h"
 
 using pw::color::color_rgb565_t;
 
diff --git a/pw_graphics/pw_framebuffer/reader_test.cc b/pw_graphics/pw_framebuffer/reader_test.cc
index 96ddbed..d7c4295 100644
--- a/pw_graphics/pw_framebuffer/reader_test.cc
+++ b/pw_graphics/pw_framebuffer/reader_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -18,10 +18,8 @@
 
 #include "gtest/gtest.h"
 #include "pw_color/color.h"
-#include "pw_color/colors_endesga32.h"
 #include "pw_color/colors_pico8.h"
 #include "pw_framebuffer/framebuffer.h"
-#include "pw_framebuffer/reader.h"
 #include "pw_framebuffer/writer.h"
 
 using pw::color::color_rgb565_t;
diff --git a/pw_graphics/pw_framebuffer/writer.cc b/pw_graphics/pw_framebuffer/writer.cc
index 9b2e705..10c20dc 100644
--- a/pw_graphics/pw_framebuffer/writer.cc
+++ b/pw_graphics/pw_framebuffer/writer.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -11,9 +11,7 @@
 // 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 "public/pw_framebuffer/writer.h"
-
-#include <sys/signal.h>
+#include "pw_framebuffer/writer.h"
 
 #include "pw_assert/assert.h"
 
diff --git a/pw_graphics/pw_framebuffer/writer_test.cc b/pw_graphics/pw_framebuffer/writer_test.cc
index 4e3b9d8..a060ad7 100644
--- a/pw_graphics/pw_framebuffer/writer_test.cc
+++ b/pw_graphics/pw_framebuffer/writer_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -18,10 +18,8 @@
 
 #include "gtest/gtest.h"
 #include "pw_color/color.h"
-#include "pw_color/colors_endesga32.h"
 #include "pw_color/colors_pico8.h"
 #include "pw_framebuffer/framebuffer.h"
-#include "pw_log/log.h"
 
 using pw::color::color_rgb565_t;
 
@@ -46,7 +44,7 @@
   uint16_t data[8 * 8];
   Framebuffer fb(data, PixelFormat::RGB565, {8, 8}, 8 * sizeof(data[0]));
   FramebufferWriter writer(fb);
-  color_rgb565_t indigo = color::colors_pico8_rgb565[12];
+  color_rgb565_t indigo = color::kColorsPico8Rgb565[12];
   writer.Fill(indigo);
   const color_rgb565_t* pixel_data =
       static_cast<const color_rgb565_t*>(fb.data());
diff --git a/pw_graphics/pw_framebuffer_pool/BUILD.bazel b/pw_graphics/pw_framebuffer_pool/BUILD.bazel
new file mode 100644
index 0000000..5505754
--- /dev/null
+++ b/pw_graphics/pw_framebuffer_pool/BUILD.bazel
@@ -0,0 +1,33 @@
+# Copyright 2024 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.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "pw_framebuffer_pool",
+    srcs = ["framebuffer_pool.cc"],
+    hdrs = ["public/pw_framebuffer_pool/framebuffer_pool.h"],
+    includes = ["public"],
+    deps = [
+        "//pw_graphics/pw_color",
+        "//pw_graphics/pw_framebuffer",
+        "//pw_graphics/pw_geometry",
+        "@pigweed//pw_assert",
+        "@pigweed//pw_containers",
+        "@pigweed//pw_status",
+        "@pigweed//pw_sync:counting_semaphore",
+    ],
+)
diff --git a/pw_graphics/pw_framebuffer_pool/BUILD.gn b/pw_graphics/pw_framebuffer_pool/BUILD.gn
index 185baf7..bb39e24 100644
--- a/pw_graphics/pw_framebuffer_pool/BUILD.gn
+++ b/pw_graphics/pw_framebuffer_pool/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2023 The Pigweed Authors
+# Copyright 2024 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
@@ -15,13 +15,19 @@
 import("//build_overrides/pigweed.gni")
 
 import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
 
-config("default_config") {
+config("public_include_path") {
   include_dirs = [ "public" ]
+  visibility = [ ":*" ]
+}
+
+pw_test_group("tests") {
 }
 
 pw_source_set("pw_framebuffer_pool") {
-  public_configs = [ ":default_config" ]
+  public_configs = [ ":public_include_path" ]
   public = [ "public/pw_framebuffer_pool/framebuffer_pool.h" ]
   deps = [
     "$dir_pw_assert",
@@ -32,7 +38,11 @@
     "$dir_pw_status",
     "$dir_pw_sync:counting_semaphore",
     "$dir_pwexperimental_framebuffer",
-    "$dir_pwexperimental_math",
+    "$dir_pwexperimental_geometry",
   ]
   sources = [ "framebuffer_pool.cc" ]
 }
+
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
+}
diff --git a/pw_graphics/pw_framebuffer_pool/CMakeLists.txt b/pw_graphics/pw_framebuffer_pool/CMakeLists.txt
new file mode 100644
index 0000000..2baccea
--- /dev/null
+++ b/pw_graphics/pw_framebuffer_pool/CMakeLists.txt
@@ -0,0 +1,30 @@
+# Copyright 2024 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_framebuffer_pool STATIC
+  SOURCES
+    framebuffer_pool.cc
+  HEADERS
+    public/pw_framebuffer_pool/framebuffer_pool.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_containers
+    pw_framebuffer
+    pw_geometry
+    pw_status
+    pw_sync.counting_semaphore
+)
diff --git a/pw_graphics/pw_framebuffer_pool/docs.rst b/pw_graphics/pw_framebuffer_pool/docs.rst
new file mode 100644
index 0000000..ce0a9bb
--- /dev/null
+++ b/pw_graphics/pw_framebuffer_pool/docs.rst
@@ -0,0 +1,20 @@
+.. _module-pw_framebuffer_pool:
+
+===================
+pw_framebuffer_pool
+===================
+.. pigweed-module::
+   :name: pw_framebuffer_pool
+
+.. seealso::
+   This module is part of SEED :ref:`seed-0104`.
+
+-------------
+API reference
+-------------
+.. doxygengroup:: pw_framebuffer_pool
+   :members:
+
+.. include:: ../pw_display/docs.rst
+   :start-after: .. graphics-modules-start
+   :end-before: .. graphics-modules-end
diff --git a/pw_graphics/pw_framebuffer_pool/framebuffer_pool.cc b/pw_graphics/pw_framebuffer_pool/framebuffer_pool.cc
index 2b5680e..f843f7c 100644
--- a/pw_graphics/pw_framebuffer_pool/framebuffer_pool.cc
+++ b/pw_graphics/pw_framebuffer_pool/framebuffer_pool.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -31,7 +31,8 @@
 
 Framebuffer FramebufferPool::GetFramebuffer() {
   framebuffer_semaphore_.acquire();
-  size_t idx = next_fb_idx_++;
+  size_t idx = next_fb_idx_;
+  next_fb_idx_ = idx + 1;
   if (next_fb_idx_ == buffer_addresses_.size())
     next_fb_idx_ = 0;
 
diff --git a/pw_graphics/pw_framebuffer_pool/public/pw_framebuffer_pool/framebuffer_pool.h b/pw_graphics/pw_framebuffer_pool/public/pw_framebuffer_pool/framebuffer_pool.h
index 4b4a621..dab9f3e 100644
--- a/pw_graphics/pw_framebuffer_pool/public/pw_framebuffer_pool/framebuffer_pool.h
+++ b/pw_graphics/pw_framebuffer_pool/public/pw_framebuffer_pool/framebuffer_pool.h
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -19,65 +19,77 @@
 
 #include "pw_containers/vector.h"
 #include "pw_framebuffer/framebuffer.h"
-#include "pw_math/size.h"
+#include "pw_geometry/size.h"
 #include "pw_status/status.h"
 #include "pw_sync/counting_semaphore.h"
 
+/// @defgroup pw_framebuffer_pool
+
 namespace pw::framebuffer_pool {
 
-// FramebufferPool manages a collection of (one or more) framebuffers.
-// It provides a mechanism to retrieve a buffer from the pool for use, and
-// for returning that buffer back to the pool.
+/// @ingroup pw_framebuffer_pool
+///
+/// FramebufferPool manages a collection of (one or more) framebuffers.
+/// It provides a mechanism to retrieve a buffer from the pool for use, and
+/// for returning that buffer back to the pool.
 class FramebufferPool {
  public:
   using BufferArray = pw::Vector<void*>;
 
-  // Constructor parameters.
+  /// Constructor parameters.
   struct Config {
-    const BufferArray& fb_addr;  // Address of each buffer in this pool.
-    pw::math::Size<uint16_t> dimensions;  // width/height of each buffer.
-    uint16_t row_bytes;                   // row bytes of each buffer.
+    /// Address of each buffer in this pool.
+    const BufferArray& fb_addr;
+    /// Width/height of each buffer.
+    pw::geometry::Size<uint16_t> dimensions;
+    /// Row bytes of each buffer.
+    uint16_t row_bytes;
+    /// Shared pixel format.
     pw::framebuffer::PixelFormat pixel_format;
   };
 
   FramebufferPool(const Config& config);
   virtual ~FramebufferPool();
 
-  // Return the framebuffer addresses for initialization purposes only.
-  // Some drivers require these during initialization of their subsystems.
-  // Do not use this as a means to retrieve the address of a framebuffer.
-  // Always use GetFramebuffer if a new buffer is needed.
+  /// Return the framebuffer addresses for initialization purposes only.
+  /// Some drivers require these during initialization of their subsystems.
+  /// Do not use this as a means to retrieve the address of a framebuffer.
+  /// Always use GetFramebuffer if a new buffer is needed.
   const BufferArray& GetBuffersForInit() const { return buffer_addresses_; }
 
-  // Return the row bytes for each framebuffer in this pool.
+  /// Return the row bytes for each framebuffer in this pool.
   uint16_t row_bytes() const { return row_bytes_; }
 
-  // Return the dimensions (width/height) for each framebuffer in this pool.
-  pw::math::Size<uint16_t> dimensions() const { return buffer_dimensions_; }
+  /// Return the dimensions (width/height) for each framebuffer in this pool.
+  pw::geometry::Size<uint16_t> dimensions() const { return buffer_dimensions_; }
 
-  // Return the pixel format for each framebuffer in this pool.
+  /// Return the pixel format for each framebuffer in this pool.
   pw::framebuffer::PixelFormat pixel_format() const { return pixel_format_; }
 
-  // Return a framebuffer to the caller for use. This call WILL BLOCK until a
-  // framebuffer is returned for use. Framebuffers *must* be returned to this
-  // pool by a corresponding call to ReleaseFramebuffer. This function will only
-  // return a valid framebuffers.
-  //
-  // This call is thread-safe, but not interrupt safe.
+  /// Return a framebuffer to the caller for use. This call WILL BLOCK until a
+  /// framebuffer is returned for use. Framebuffers *must* be returned to this
+  /// pool by a corresponding call to ReleaseFramebuffer. This function will
+  /// only return a valid framebuffers.
+  ///
+  /// This call is thread-safe, but not interrupt safe.
   virtual pw::framebuffer::Framebuffer GetFramebuffer();
 
-  // Return the framebuffer to the pool available for use by the next call to
-  // GetFramebuffer.
-  //
-  // This may be called on another thread or during an interrupt.
+  /// Return the framebuffer to the pool available for use by the next call to
+  /// GetFramebuffer.
+  ///
+  /// This may be called on another thread or during an interrupt.
   virtual Status ReleaseFramebuffer(pw::framebuffer::Framebuffer framebuffer);
 
  private:
   pw::sync::CountingSemaphore framebuffer_semaphore_;
-  const BufferArray& buffer_addresses_;         // Address of each pixel buffer.
-  pw::math::Size<uint16_t> buffer_dimensions_;  // width/height of all buffers
-  uint16_t row_bytes_;                          // All row bytes are the same.
-  pw::framebuffer::PixelFormat pixel_format_;   // Shared pixel format.
+  /// Address of each pixel buffer.
+  const BufferArray& buffer_addresses_;
+  /// Width/height of all buffers
+  pw::geometry::Size<uint16_t> buffer_dimensions_;
+  /// All row bytes are the same.
+  uint16_t row_bytes_;
+  /// Shared pixel format.
+  pw::framebuffer::PixelFormat pixel_format_;
   volatile size_t next_fb_idx_;
 };
 
diff --git a/pw_graphics/pw_geometry/BUILD.bazel b/pw_graphics/pw_geometry/BUILD.bazel
new file mode 100644
index 0000000..a4b2fbd
--- /dev/null
+++ b/pw_graphics/pw_geometry/BUILD.bazel
@@ -0,0 +1,26 @@
+# Copyright 2024 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.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "pw_geometry",
+    hdrs = [
+        "public/pw_geometry/size.h",
+        "public/pw_geometry/vector2.h",
+    ],
+    includes = ["public"],
+)
diff --git a/pw_graphics/pw_math/BUILD.gn b/pw_graphics/pw_geometry/BUILD.gn
similarity index 69%
rename from pw_graphics/pw_math/BUILD.gn
rename to pw_graphics/pw_geometry/BUILD.gn
index 2d67dfd..bb1e858 100644
--- a/pw_graphics/pw_math/BUILD.gn
+++ b/pw_graphics/pw_geometry/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2024 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
@@ -18,15 +18,22 @@
 import("$dir_pw_docgen/docs.gni")
 import("$dir_pw_unit_test/test.gni")
 
-config("default_config") {
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
+}
+
+config("public_include_path") {
   include_dirs = [ "public" ]
 }
 
-pw_source_set("pw_math") {
-  public_configs = [ ":default_config" ]
+pw_source_set("pw_geometry") {
+  public_configs = [ ":public_include_path" ]
   public = [
-    "public/pw_math/size.h",
-    "public/pw_math/vector2.h",
-    "public/pw_math/vector3.h",
+    "public/pw_geometry/size.h",
+    "public/pw_geometry/vector2.h",
+    "public/pw_geometry/vector3.h",
   ]
 }
+
+pw_test_group("tests") {
+}
diff --git a/pw_graphics/pw_geometry/CMakeLists.txt b/pw_graphics/pw_geometry/CMakeLists.txt
new file mode 100644
index 0000000..2bd5049
--- /dev/null
+++ b/pw_graphics/pw_geometry/CMakeLists.txt
@@ -0,0 +1,23 @@
+# Copyright 2024 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_geometry INTERFACE
+  HEADERS
+    public/pw_geometry/size.h
+    public/pw_geometry/vector2.h
+  PUBLIC_INCLUDES
+    public
+)
diff --git a/pw_graphics/pw_geometry/OWNERS b/pw_graphics/pw_geometry/OWNERS
new file mode 100644
index 0000000..2aa396f
--- /dev/null
+++ b/pw_graphics/pw_geometry/OWNERS
@@ -0,0 +1,2 @@
+konkers@google.com
+tonymd@google.com
diff --git a/pw_graphics/pw_geometry/docs.rst b/pw_graphics/pw_geometry/docs.rst
new file mode 100644
index 0000000..1b643f6
--- /dev/null
+++ b/pw_graphics/pw_geometry/docs.rst
@@ -0,0 +1,23 @@
+.. _module-pw_geometry:
+
+===========
+pw_geometry
+===========
+.. pigweed-module::
+   :name: pw_geometry
+
+.. seealso::
+   This module is part of SEED :ref:`seed-0104`.
+
+``pw_geometry`` contains two helper structures for common values usually used as
+a pair.
+
+-------------
+API reference
+-------------
+.. doxygengroup:: pw_geometry
+   :members:
+
+.. include:: ../pw_display/docs.rst
+   :start-after: .. graphics-modules-start
+   :end-before: .. graphics-modules-end
diff --git a/pw_graphics/pw_math/public/pw_math/size.h b/pw_graphics/pw_geometry/public/pw_geometry/size.h
similarity index 78%
rename from pw_graphics/pw_math/public/pw_math/size.h
rename to pw_graphics/pw_geometry/public/pw_geometry/size.h
index 8831721..8bf5101 100644
--- a/pw_graphics/pw_math/public/pw_math/size.h
+++ b/pw_graphics/pw_geometry/public/pw_geometry/size.h
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -13,8 +13,13 @@
 // the License.
 #pragma once
 
-namespace pw::math {
+/// @defgroup pw_geometry
 
+namespace pw::geometry {
+
+/// @ingroup pw_geometry
+///
+/// Container class for width and height. Commonly used within pw::framebuffer.
 template <typename T>
 struct Size {
   T width;
@@ -28,4 +33,4 @@
   }
 };
 
-}  // namespace pw::math
+}  // namespace pw::geometry
diff --git a/pw_graphics/pw_math/public/pw_math/vector2.h b/pw_graphics/pw_geometry/public/pw_geometry/vector2.h
similarity index 86%
rename from pw_graphics/pw_math/public/pw_math/vector2.h
rename to pw_graphics/pw_geometry/public/pw_geometry/vector2.h
index ed43216..1475240 100644
--- a/pw_graphics/pw_math/public/pw_math/vector2.h
+++ b/pw_graphics/pw_geometry/public/pw_geometry/vector2.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2024 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
@@ -13,7 +13,7 @@
 // the License.
 #pragma once
 
-namespace pw::math {
+namespace pw::geometry {
 
 template <typename T>
 struct Vector2 {
@@ -21,4 +21,4 @@
   T y;
 };
 
-}  // namespace pw::math
+}  // namespace pw::geometry
diff --git a/pw_graphics/pw_math/public/pw_math/vector3.h b/pw_graphics/pw_geometry/public/pw_geometry/vector3.h
similarity index 88%
rename from pw_graphics/pw_math/public/pw_math/vector3.h
rename to pw_graphics/pw_geometry/public/pw_geometry/vector3.h
index 5a9eaae..a45d760 100644
--- a/pw_graphics/pw_math/public/pw_math/vector3.h
+++ b/pw_graphics/pw_geometry/public/pw_geometry/vector3.h
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -13,7 +13,7 @@
 // the License.
 #pragma once
 
-namespace pw::math {
+namespace pw::geometry {
 
 template <typename T>
 struct Vector3 {
@@ -24,4 +24,4 @@
   T z;
 };
 
-}  // namespace pw::math
+}  // namespace pw::geometry
diff --git a/pw_mipi_dsi_mcuxpresso/device.cc b/pw_mipi_dsi_mcuxpresso/device.cc
index e55fc55..127d19d 100644
--- a/pw_mipi_dsi_mcuxpresso/device.cc
+++ b/pw_mipi_dsi_mcuxpresso/device.cc
@@ -60,9 +60,10 @@
 
 }  // namespace
 
-MCUXpressoDevice::MCUXpressoDevice(const FramebufferPool& framebuffer_pool,
-                                   const pw::math::Size<uint16_t>& panel_size,
-                                   video_pixel_format_t pixel_format)
+MCUXpressoDevice::MCUXpressoDevice(
+    const FramebufferPool& framebuffer_pool,
+    const pw::geometry::Size<uint16_t>& panel_size,
+    video_pixel_format_t pixel_format)
     : framebuffer_pool_(framebuffer_pool),
       fbdev_(kVideoLayer),
       dsi_device_({
diff --git a/pw_mipi_dsi_mcuxpresso/public/pw_mipi_dsi_mcuxpresso/device.h b/pw_mipi_dsi_mcuxpresso/public/pw_mipi_dsi_mcuxpresso/device.h
index 2bad62d..ad7b35f 100644
--- a/pw_mipi_dsi_mcuxpresso/public/pw_mipi_dsi_mcuxpresso/device.h
+++ b/pw_mipi_dsi_mcuxpresso/public/pw_mipi_dsi_mcuxpresso/device.h
@@ -20,7 +20,7 @@
 #include "fsl_rm67162.h"
 #include "pw_color/color.h"
 #include "pw_framebuffer_pool/framebuffer_pool.h"
-#include "pw_math/size.h"
+#include "pw_geometry/size.h"
 #include "pw_mipi_dsi/device.h"
 #include "pw_mipi_dsi_mcuxpresso/framebuffer_device.h"
 #include "pw_status/status.h"
@@ -40,7 +40,7 @@
  public:
   MCUXpressoDevice(
       const pw::framebuffer_pool::FramebufferPool& framebuffer_pool,
-      const pw::math::Size<uint16_t>& panel_size,
+      const pw::geometry::Size<uint16_t>& panel_size,
       video_pixel_format_t pixel_format);
   virtual ~MCUXpressoDevice();
 
diff --git a/pw_pixel_pusher/BUILD.bazel b/pw_pixel_pusher/BUILD.bazel
new file mode 100644
index 0000000..4642deb
--- /dev/null
+++ b/pw_pixel_pusher/BUILD.bazel
@@ -0,0 +1,30 @@
+# Copyright 2024 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.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "pw_pixel_pusher",
+    hdrs = ["public/pw_pixel_pusher/pixel_pusher.h"],
+    includes = ["public"],
+    deps = [
+        "//pw_graphics/pw_framebuffer",
+        "//pw_graphics/pw_framebuffer_pool",
+        "@pigweed//pw_bytes",
+        "@pigweed//pw_span",
+        "@pigweed//pw_status",
+    ],
+)
diff --git a/pw_pixel_pusher/BUILD.gn b/pw_pixel_pusher/BUILD.gn
index 5811f93..fe23afa 100644
--- a/pw_pixel_pusher/BUILD.gn
+++ b/pw_pixel_pusher/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2023 The Pigweed Authors
+# Copyright 2024 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
@@ -15,17 +15,18 @@
 import("//build_overrides/pigweed.gni")
 
 import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
 
 config("public_include_path") {
   include_dirs = [ "public" ]
   visibility = [ ":*" ]
 }
 
-group("pw_pixel_pusher") {
-  deps = [ ":pixel_pusher" ]
+pw_test_group("tests") {
 }
 
-pw_source_set("pixel_pusher") {
+pw_source_set("pw_pixel_pusher") {
   public_configs = [ ":public_include_path" ]
   public = [ "public/pw_pixel_pusher/pixel_pusher.h" ]
   public_deps = [
@@ -36,3 +37,7 @@
     "$dir_pwexperimental_framebuffer_pool",
   ]
 }
+
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
+}
diff --git a/pw_pixel_pusher/CMakeLists.txt b/pw_pixel_pusher/CMakeLists.txt
new file mode 100644
index 0000000..d46349e
--- /dev/null
+++ b/pw_pixel_pusher/CMakeLists.txt
@@ -0,0 +1,28 @@
+# Copyright 2024 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_pixel_pusher INTERFACE
+  HEADERS
+    public/pw_pixel_pusher/pixel_pusher.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_bytes
+    pw_framebuffer
+    pw_framebuffer_pool
+    pw_span
+    pw_status
+)
diff --git a/pw_pixel_pusher/docs.rst b/pw_pixel_pusher/docs.rst
new file mode 100644
index 0000000..3dcb986
--- /dev/null
+++ b/pw_pixel_pusher/docs.rst
@@ -0,0 +1,20 @@
+.. _module-pw_pixel_pusher:
+
+===============
+pw_pixel_pusher
+===============
+.. pigweed-module::
+   :name: pw_pixel_pusher
+
+.. seealso::
+   This module is part of SEED :ref:`seed-0104`.
+
+-------------
+API reference
+-------------
+.. doxygengroup:: pw_pixel_pusher
+   :members:
+
+.. include:: ../pw_display/docs.rst
+   :start-after: .. graphics-modules-start
+   :end-before: .. graphics-modules-end
diff --git a/pw_pixel_pusher/public/pw_pixel_pusher/pixel_pusher.h b/pw_pixel_pusher/public/pw_pixel_pusher/pixel_pusher.h
index 6a6df0a..cd551a3 100644
--- a/pw_pixel_pusher/public/pw_pixel_pusher/pixel_pusher.h
+++ b/pw_pixel_pusher/public/pw_pixel_pusher/pixel_pusher.h
@@ -1,4 +1,4 @@
-// Copyright 2023 The Pigweed Authors
+// Copyright 2024 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
@@ -18,15 +18,19 @@
 #include "pw_function/function.h"
 #include "pw_status/status.h"
 
+/// @defgroup pw_pixel_pusher
+
 namespace pw::pixel_pusher {
 
+/// @ingroup pw_pixel_pusher
+///
+/// A class responsible for writing a framebuffer to a display.
 class PixelPusher {
  public:
   using WriteCallback = Callback<void(framebuffer::Framebuffer, Status)>;
 
   virtual ~PixelPusher() = default;
 
-  // PixelPusher implementation:
   virtual Status Init(
       const pw::framebuffer_pool::FramebufferPool& framebuffer_pool) = 0;
 
diff --git a/pw_pixel_pusher_rp2040_pio/BUILD.bazel b/pw_pixel_pusher_rp2040_pio/BUILD.bazel
new file mode 100644
index 0000000..b3d4f37
--- /dev/null
+++ b/pw_pixel_pusher_rp2040_pio/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 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.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# Bazel is not yet supported for the rp2040.
+filegroup(
+    name = "sources",
+    srcs = [
+        "pixel_pusher.cc",
+        "public/pw_pixel_pusher_rp2040_pio/pixel_pusher.h",
+        "public/pw_pixel_pusher_rp2040_pio/st7789.pio.h",
+    ],
+)
diff --git a/pw_pixel_pusher_rp2040_pio/BUILD.gn b/pw_pixel_pusher_rp2040_pio/BUILD.gn
index e4aa52e..21d978f 100644
--- a/pw_pixel_pusher_rp2040_pio/BUILD.gn
+++ b/pw_pixel_pusher_rp2040_pio/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2023 The Pigweed Authors
+# Copyright 2024 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
@@ -16,26 +16,37 @@
 import("//build_overrides/pigweed.gni")
 
 import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
 
-config("default_config") {
+config("public_include_path") {
   include_dirs = [ "public" ]
+  visibility = [ ":*" ]
+}
+
+pw_test_group("tests") {
 }
 
 pw_source_set("pw_pixel_pusher_rp2040_pio") {
-  public_configs = [ ":default_config" ]
-  deps = [
-    "$dir_pw_function",
-    "$dir_pw_log",
+  public_configs = [ ":public_include_path" ]
+  public = [
+    "public/pw_pixel_pusher_rp2040_pio/pixel_pusher.h",
+    "public/pw_pixel_pusher_rp2040_pio/st7789.pio.h",
   ]
+  sources = [ "pixel_pusher.cc" ]
   public_deps = [
     "$PICO_ROOT/src/rp2_common/hardware_dma",
     "$PICO_ROOT/src/rp2_common/hardware_pio",
-    "$PICO_ROOT/src/rp2_common/hardware_spi",
+  ]
+  deps = [
+    "$PICO_ROOT/src/rp2_common/hardware_irq",
     "$dir_pw_digital_io",
     "$dir_pw_sync:binary_semaphore",
     "$dir_pwexperimental_framebuffer_pool",
-    "$dir_pwexperimental_pixel_pusher:pixel_pusher",
+    "$dir_pwexperimental_pixel_pusher",
   ]
-  sources = [ "pixel_pusher.cc" ]
-  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
+
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
 }
diff --git a/pw_pixel_pusher_rp2040_pio/CMakeLists.txt b/pw_pixel_pusher_rp2040_pio/CMakeLists.txt
new file mode 100644
index 0000000..5ce6a20
--- /dev/null
+++ b/pw_pixel_pusher_rp2040_pio/CMakeLists.txt
@@ -0,0 +1,17 @@
+# Copyright 2024 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Cmake is not supported for the rp2040.
diff --git a/pw_pixel_pusher_rp2040_pio/docs.rst b/pw_pixel_pusher_rp2040_pio/docs.rst
new file mode 100644
index 0000000..786c1d2
--- /dev/null
+++ b/pw_pixel_pusher_rp2040_pio/docs.rst
@@ -0,0 +1,21 @@
+.. _module-pw_pixel_pusher_rp2040_pio:
+
+==========================
+pw_pixel_pusher_rp2040_pio
+==========================
+.. pigweed-module::
+   :name: pw_pixel_pusher_rp2040_pio
+
+.. seealso::
+   This module is part of SEED :ref:`seed-0104`.
+
+-------------
+API reference
+-------------
+.. doxygengroup:: pw_pixel_pusher_rp2040_pio
+   :members:
+
+.. include:: ../pw_display/docs.rst
+   :start-after: .. graphics-modules-start
+   :end-before: .. graphics-modules-end
+
diff --git a/pw_pixel_pusher_rp2040_pio/pixel_pusher.cc b/pw_pixel_pusher_rp2040_pio/pixel_pusher.cc
index b4ce090..b6af063 100644
--- a/pw_pixel_pusher_rp2040_pio/pixel_pusher.cc
+++ b/pw_pixel_pusher_rp2040_pio/pixel_pusher.cc
@@ -18,8 +18,8 @@
 #include <cstddef>
 
 #include "hardware/dma.h"
+#include "hardware/irq.h"
 #include "hardware/pio.h"
-#include "hardware/spi.h"
 #include "pw_digital_io/digital_io.h"
 #include "pw_pixel_pusher_rp2040_pio/st7789.pio.h"
 
diff --git a/pw_pixel_pusher_rp2040_pio/public/pw_pixel_pusher_rp2040_pio/pixel_pusher.h b/pw_pixel_pusher_rp2040_pio/public/pw_pixel_pusher_rp2040_pio/pixel_pusher.h
index 51e9f55..4d9b3ba 100644
--- a/pw_pixel_pusher_rp2040_pio/public/pw_pixel_pusher_rp2040_pio/pixel_pusher.h
+++ b/pw_pixel_pusher_rp2040_pio/public/pw_pixel_pusher_rp2040_pio/pixel_pusher.h
@@ -19,17 +19,21 @@
 #include "hardware/dma.h"
 #include "hardware/irq.h"
 #include "hardware/pio.h"
-#include "hardware/spi.h"
-#include "pw_digital_io/digital_io.h"
 #include "pw_pixel_pusher/pixel_pusher.h"
 #include "pw_sync/binary_semaphore.h"
 
+/// @defgroup pw_pixel_pusher_rp2040_pio
+
 namespace pw::pixel_pusher {
 
+/// @ingroup pw_pixel_pusher_rp2040_pio
+///
+/// Implementation of pw_pixel_pusher that uses the RP2040 PIO block to write
+/// pixels over SPI with pixel doubling.
 class PixelPusherRp2040Pio : public PixelPusher {
  public:
   PixelPusherRp2040Pio(
-      int dc_pin, int cs_pin, int dout_pin, int sck_pin_, int te_pin, PIO pio);
+      int dc_pin, int cs_pin, int dout_pin, int sck_pin, int te_pin, PIO pio);
   ~PixelPusherRp2040Pio();
 
   // PixelPusher implementation:
@@ -47,15 +51,15 @@
   bool VsyncCallback(gpio_irq_callback_t callback);
 
  private:
-  bool pixel_double_enabled_ = false;
-  bool write_mode_ = false;
-  PIO pio_;
-  uint dma_channel_;
   uint dc_pin_;
   uint cs_pin_;
-  uint te_pin_;
   uint dout_pin_;
   uint sck_pin_;
+  uint te_pin_;
+  PIO pio_;
+  bool pixel_double_enabled_ = false;
+  bool write_mode_ = false;
+  uint dma_channel_;
   uint pio_sm_;
   uint pio_offset_;
   uint pio_double_offset_;