From 50bc9325a476f49fa166a2cbd9609cf5094a5ea0 Mon Sep 17 00:00:00 2001 From: Jason Huebel Date: Mon, 25 May 2026 16:00:00 -0500 Subject: [PATCH 1/3] =?UTF-8?q?docs:=20update=20USER=5FGUIDE.md=20for=20v1?= =?UTF-8?q?.1.0=E2=80=93v1.3.0=20features=20(#2134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Uri Tauber --- USER_GUIDE.md | 146 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 129 insertions(+), 17 deletions(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 827e85ea6c..e2d97c9f81 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -24,12 +24,17 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control - [3.6.6 Web Settings (WiFi + OPDS)](#366-web-settings-wifi--opds) - [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup) - [3.7 Sleep Screen](#37-sleep-screen) + - [3.8 Custom Fonts (SD Card)](#38-custom-fonts-sd-card) - [4. Reading Mode](#4-reading-mode) - [Page Turning](#page-turning) - [Chapter Navigation](#chapter-navigation) + - [Auto Page Turn](#auto-page-turn) + - [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only) + - [Footnote Navigation](#footnote-navigation) - [System Navigation](#system-navigation) - [Supported Languages](#supported-languages) - - [5. Chapter Selection Screen](#5-chapter-selection-screen) + - [5. Reader Menu](#5-reader-menu) + - [5.1 Chapter Selection](#51-chapter-selection) - [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap) - [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop) @@ -83,11 +88,12 @@ See [Reading Mode](#4-reading-mode) below for more information. ### 3.3 Browse Files Screen -The Browse Files screen acts as a file and folder browser. +The Browse Files screen acts as a file and folder browser. The full path to the current directory is shown at the top of the screen. File extensions are displayed alongside each filename, and directories are shown with brackets (e.g. `[folder-name]`). Hidden directories (those beginning with `.`) are also visible. * **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down. -* **Open Selection:** Press **Confirm** to open a folder or read a selected book. -* **Delete Files:** Hold and release **Confirm** to delete the selected file. You will be given an option to either confirm or cancel deletion. Folder deletion is not supported. +* **Open Selection:** Press **Confirm** to open a folder or start reading a selected book. Selecting a `.bmp` file will open the image viewer. +* **Delete Files or Folders:** Hold and release **Confirm** to delete the selected file or folder. You will be given an option to either confirm or cancel. Multiple files can be selected for deletion in a single operation. +* **Rename or Move:** Files can be renamed or moved to a different folder from within the browse screen. ### 3.4 Recent Books Screen @@ -99,8 +105,16 @@ The File Transfer screen allows you to upload new e-books to the device. When yo See the [webserver docs](./docs/webserver.md) for more information on how to connect to the web server and upload files. +The web interface also supports **WebDAV**, allowing you to mount the device as a network drive and manage files directly from your computer's file manager. + +Download links for files already on the device are available in the web interface, so you can retrieve books or screenshots over WiFi without connecting a cable. + +A **WiFi signal strength indicator** (dBm) is displayed on-screen during web server sessions. + > [!TIP] > Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details. +> [!TIP] +> If your EPUBs have compatibility issues, you can run the built-in **EPUB Optimizer** directly from the device to clean up and reprocess books for better rendering. ### 3.5.1 Calibre Wireless Transfers @@ -126,7 +140,7 @@ The Settings screen allows you to configure the device's behavior. There are a f - "Custom" - Custom images from the SD card; see [Sleep Screen](#37-sleep-screen) below for more information - "Cover" - The book cover image (Note: this is experimental and may not work as expected) - "None" - A blank screen - - "Cover + Custom" - The book cover image, falls back to "Custom" behavior + - "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise - **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected: - "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary - "Crop" - Scale the image down and crop as necessary to try to fill the screen (Note: this is experimental and may not work as expected) @@ -151,10 +165,14 @@ The Settings screen allows you to configure the device's behavior. There are a f - "Classic" - The original Crosspoint theme - "Lyra" - The new theme for Crosspoint featuring rounded elements and menu icons - "Lyra Extended" - Lyra, but displays 3 books instead of 1 on the **[Home Screen](#31-home-screen)** + - "RoundedRaff" - A rounded theme with additional visual styling - **Sunlight Fading Fix**: Configure whether to enable a software-fix for the issue where white X4 models may fade when used in direct sunlight: - "OFF" (default) - Disable the fix - "ON" - Enable the fix +> [!NOTE] +> A battery charging indicator is shown on the battery icon whenever the device is actively charging. + #### 3.6.2 Reader - **Reader Font Family**: Choose the font used for reading: - "Noto Serif" (default) - Google's serif font @@ -176,6 +194,8 @@ The Settings screen allows you to configure the device's behavior. There are a f - "ON" - Vertical space will be added between paragraphs in Reading Mode - "OFF" - Paragraphs will not have vertical space added, but will have first-line indentation - **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly. +- **Images**: Whether to display embedded images (JPG/PNG) found in EPUB files; options are "ON" (default) or "OFF". +- **Focus Reading**: Bolds the first part of each word to create visual fixation points, similar to Bionic Reading. This can help improve reading speed and focus; options are "ON" or "OFF" (default). #### 3.6.3 Controls @@ -189,17 +209,19 @@ The Settings screen allows you to configure the device's behavior. There are a f - "Ignore" (default) - Require a long press to turn off the device - "Sleep" - A short press puts the device into sleep mode - "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off + - "Refresh" - A short press triggers a manual full-screen refresh, useful for clearing ghosting #### 3.6.4 System -- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep; options are 1, 5, 10 (default), 15 or 30 minutes. +- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep; options are 1, 3, 5, 10 (default), 15 or 30 minutes. - **WiFi Networks**: Connect to WiFi networks for file transfers and firmware updates. - **KOReader Sync**: Options for setting up KOReader for syncing book progress. - **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below. - **Clear Reading Cache**: Clear the internal SD card cache. -- **Check for updates**: Check for Crosspoint firmware updates over WiFi. -- **Language**: Set the system language (see **[Supported Languages](#supported-languages)** for more information). +- **Check for updates**: Check for Crosspoint firmware updates over WiFi. Firmware can also be updated without a USB connection by placing a `firmware.bin` file on the SD card. +- **Language**: Set the UI language. CrossPoint supports 22 languages including English, Spanish, French, German, Czech, Portuguese, Russian, Swedish, Turkish, Danish, Finnish, Polish, Dutch, Belarusian, Italian, Ukrainian, Romanian, Catalan, Vietnamese, Kazakh, Slovenian, and more. +- **Manage Fonts**: Browse, download, and manage custom font families installed from the SD card. See [Custom Fonts (SD Card)](#38-custom-fonts-sd-card) for more information. #### 3.6.5 OPDS Servers (Multiple Libraries) @@ -358,7 +380,7 @@ The **Sleep Screen** setting controls what is displayed when the device goes to | **Light** | The CrossPoint logo on a white background. | | **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. | | **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. | -| **Cover + Custom** | The cover of the currently open book. Falls back to **Custom** behavior if no book is open. | +| **Cover + Custom** | The cover of the currently open book, shown only while actively reading. Falls back to **Custom** behavior when not reading. | | **None** | A blank screen. | #### Cover settings @@ -381,6 +403,25 @@ To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + > - X4: Use a resolution of 480x800 pixels to match the device's screen resolution. > - X3: Use a resolution of 528x792 pixels to match the device's screen resolution. +> [!TIP] +> You can set an image as the sleep screen cover directly from the BMP image viewer in the **[Browse Files](#33-browse-files-screen)** screen. + +--- + +### 3.8 Custom Fonts (SD Card) + +CrossPoint supports loading additional fonts from the SD card, extending beyond the three built-in families (Noto Serif, Noto Sans, Open Dyslexic). Custom fonts can include extended Unicode coverage, enabling CJK (Chinese, Japanese, Korean) and other scripts. + +There are three ways to install fonts: + +1. **Download from device (recommended):** Go to **Settings → System → Manage Fonts**, browse the available font families, and select one to download over WiFi. +2. **Upload via web interface:** While in **File Transfer** mode, open the web UI in a browser and navigate to the **Fonts** tab to upload `.cpfont` files. +3. **Manual SD card copy:** Download font files from the [crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts) and copy them to `/.fonts/` (preferred) or `/fonts/` on your SD card. + +Once installed, custom fonts appear in **Settings → Reader → Font Family** alongside the built-in fonts. + +See [docs/sd-card-fonts.md](./docs/sd-card-fonts.md) for full installation details and SD card folder structure. + --- ## 4. Reading Mode @@ -403,26 +444,57 @@ If the **Short Power Button Click** setting is set to "Page Turn", you can also This feature can be disabled in the **[Controls Settings](#363-controls)** to help avoid changing chapters by mistake. +### Auto Page Turn + +Auto Page Turn automatically advances pages at a set interval, useful for hands-free reading. This feature can be enabled and configured from the **[Reader Menu](#5-reader-menu)** while reading an EPUB. + +### Tilt Page Turn (X3 only) + +On the **Xteink X3**, the gyroscope can be used to turn pages by tilting the device. This feature is available in the Controls settings. + +### Footnote Navigation + +When reading an EPUB that contains footnotes, you can navigate to the footnote text by selecting the footnote reference in the book. From the footnote, you can return to your original reading position. ### System Navigation * **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen. * **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen. -* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)** screen. +* **Reader Menu:** Press **Confirm** to open the **[Reader Menu](#5-reader-menu)**, which includes chapter navigation, reading options, and more. ### Supported Languages CrossPoint renders text using the following Unicode character blocks, enabling support for a wide range of languages: -* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others. +* **Latin Script (Basic, Supplement, Extended-A/B):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, Catalan, and others. * **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others. +* **Vietnamese:** Supported via extended Latin glyph coverage in the built-in fonts. -What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi. +What is not supported with built-in fonts: Chinese, Japanese, Korean, Hebrew, Arabic, Greek, and Farsi. However, **CJK and other extended scripts can be enabled by installing custom SD card fonts** — see [Custom Fonts (SD Card)](#38-custom-fonts-sd-card). --- -## 5. Chapter Selection Screen +## 5. Reader Menu + +Press **Confirm** while reading to open the Reader Menu. From here you can access reading utilities and navigation options without leaving the book. + +Available options include: + +- **Select Chapter** – Open the table of contents to jump to a specific chapter (see [Chapter Selection](#51-chapter-selection) below). +- **Footnotes** – Navigate to the footnotes for the current section *(only shown in books that contain footnotes)*. +- **Reading Orientation** – Cycle through screen orientations without leaving the reader. +- **Auto Turn (Pages Per Minute)** – Cycle through automatic page turn speed options for hands-free reading. +- **Go to %** – Jump to a specific position in the book by percentage. +- **Take screenshot** – Save a screenshot of the current page to the `screenshots/` folder. +- **Show page as QR** – Display a QR code encoding the current reading position. +- **Go Home** – Close the book and return to the Home screen. +- **Sync Progress** – Push or pull reading progress with a KOReader sync server (see [KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)). +- **Delete Book Cache** – Clear the cached layout data for the current book, forcing a re-index on next open. + +Press **Back** at any time to close the menu and return to your current page. -Accessible by pressing **Confirm** while inside a book. +### 5.1 Chapter Selection + +Accessible by selecting **Chapters** from the Reader Menu. 1. Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to highlight the desired chapter. 2. Press **Confirm** to jump to that chapter. @@ -434,19 +506,59 @@ Accessible by pressing **Confirm** while inside a book. Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates: -* **Images:** Embedded images in e-books will not render. * **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up. +* **Unsupported Image Formats:** Most JPG and PNG images in EPUBs render correctly. GIFs and progressive JPEGs are not supported and will fall back to an `[Image]` placeholder. +* **RTL Scripts:** Arabic, Hebrew, and Farsi are not yet supported. +* **Bookmarks:** Not yet implemented. +* **Dictionary Lookup:** Inline word lookup is not yet implemented. --- ## 7. Troubleshooting Issues & Escaping Bootloop -If an issue or crash is encountered while using Crosspoint, feel free to raise an issue ticket and attach the serial monitor logs. The logs can be obtained by connecting the device to a computer and starting a serial monitor. Either [Serial Monitor](https://www.serialmonitor.org/) or the following command can be used: +If an issue or crash is encountered while using Crosspoint, feel free to raise an issue ticket and attach the logs. + +**Crash reports on SD card:** After a crash, CrossPoint automatically saves a crash report to the SD card (no USB connection needed). Check the root of the SD card for a crash log file and include it with any bug report. + +**Serial monitor logs:** For more detailed debugging, connect the device to a computer and run the custom debugging monitor script (requires Python 3 with `pyserial`, `colorama`, and `matplotlib`; install via `pip3 install pyserial colorama matplotlib`): + +``` +python3 scripts/debugging_monitor.py +``` + +The script auto-detects the serial port. You can also specify one explicitly: + +``` +python3 scripts/debugging_monitor.py /dev/ttyACM0 # Linux +python3 scripts/debugging_monitor.py /dev/tty.usbmodem1 # macOS +python3 scripts/debugging_monitor.py COM7 # Windows +``` + +**Features:** +- Color-coded log output by category (errors, memory, display, EPUB parsing, etc.) +- Live memory usage graph (free RAM, total RAM, max contiguous allocation) updated every second +- Interactive command prompt — type a command and press Enter to send it to the device +- Screenshot capture — saves the current display to `screenshot.bmp` when triggered by the device + +**Options:** + +| Option | Description | +|--------|-------------| +| `--baud RATE` | Baud rate (default: 115200) | +| `--filter KEYWORD` | Show only lines containing the keyword (case-insensitive) | +| `--suppress KEYWORD` | Hide lines containing the keyword (case-insensitive) | +**Examples:** ``` -pio device monitor +# Show only memory-related log lines +python3 scripts/debugging_monitor.py --filter MEM + +# Hide noisy SD card log lines +python3 scripts/debugging_monitor.py --suppress "[SD]" ``` +Press **Ctrl-C** or close the graph window to exit. + If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen. There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder). From 67973166e30df5c75898048c9dfc69a1c6cb8dd7 Mon Sep 17 00:00:00 2001 From: Joseph DiGiovanni <37851452+Joseph-DiGiovanni@users.noreply.github.com> Date: Mon, 25 May 2026 18:00:27 -0400 Subject: [PATCH 2/3] feat: Allow disabling side buttons in reader (#2105) --- USER_GUIDE.md | 2 +- lib/I18n/translations/english.yaml | 1 + src/CrossPointSettings.h | 5 ++-- src/MappedInputManager.cpp | 38 +++++++++++++++--------------- src/SettingsList.h | 3 ++- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index e2d97c9f81..681e308850 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -200,7 +200,7 @@ The Settings screen allows you to configure the device's behavior. There are a f #### 3.6.3 Controls - **Remap Front Buttons**: A menu for customising the function of each bottom edge button. -- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from "Prev/Next" (default) to "Next/Prev". This change is only in effect when reading. +- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from "Prev/Next" (default) to "Next/Prev". You can also disable them entirely. This change is only in effect when reading. - **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skips to the next/previous chapter: - "Chapter Skip" (default) - Long-pressing skips to next/previous chapter diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 9afbfec4c5..8852b95d0d 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -140,6 +140,7 @@ STR_INVERTED: "Inverted" STR_LANDSCAPE_CCW: "Landscape CCW" STR_PREV_NEXT: "Prev/Next" STR_NEXT_PREV: "Next/Prev" +STR_DISABLED: "Disabled" STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 2be537cf1c..c4bbc6b52f 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -94,9 +94,8 @@ class CrossPointSettings { }; // Side button layout options - // Default: Previous, Next - // Swapped: Next, Previous - enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTON_LAYOUT_COUNT }; + // Default: Up = Previous, Down = Next + enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTONS_DISABLED = 2, SIDE_BUTTON_LAYOUT_COUNT }; // Font family options (built-in fonts only; SD card fonts use sdFontFamilyName) enum FONT_FAMILY { NOTOSERIF = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT }; diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index 8d8833fc99..81bc5d87c5 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -2,24 +2,8 @@ #include "CrossPointSettings.h" -namespace { -using ButtonIndex = uint8_t; - -struct SideLayoutMap { - ButtonIndex pageBack; - ButtonIndex pageForward; -}; - -// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT. -constexpr SideLayoutMap kSideLayouts[] = { - {HalGPIO::BTN_UP, HalGPIO::BTN_DOWN}, - {HalGPIO::BTN_DOWN, HalGPIO::BTN_UP}, -}; -} // namespace - bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const { - const auto sideLayout = static_cast(SETTINGS.sideButtonLayout); - const auto& side = kSideLayouts[sideLayout]; + const auto sideLayout = SETTINGS.sideButtonLayout; switch (button) { case Button::Back: @@ -45,10 +29,26 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint return (gpio.*fn)(HalGPIO::BTN_POWER); case Button::PageBack: // Reader page navigation uses side buttons and can be swapped via settings. - return (gpio.*fn)(side.pageBack); + switch (sideLayout) { + case CrossPointSettings::PREV_NEXT: + return (gpio.*fn)(HalGPIO::BTN_UP); + case CrossPointSettings::NEXT_PREV: + return (gpio.*fn)(HalGPIO::BTN_DOWN); + case CrossPointSettings::SIDE_BUTTONS_DISABLED: + default: + return false; + } case Button::PageForward: // Reader page navigation uses side buttons and can be swapped via settings. - return (gpio.*fn)(side.pageForward); + switch (sideLayout) { + case CrossPointSettings::PREV_NEXT: + return (gpio.*fn)(HalGPIO::BTN_DOWN); + case CrossPointSettings::NEXT_PREV: + return (gpio.*fn)(HalGPIO::BTN_UP); + case CrossPointSettings::SIDE_BUTTONS_DISABLED: + default: + return false; + } } return false; diff --git a/src/SettingsList.h b/src/SettingsList.h index 8ccb8614d6..19c31c3f59 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -165,7 +165,8 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist "imageRendering", StrId::STR_CAT_READER), // --- Controls --- SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout, - {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS), + {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV, StrId::STR_DISABLED}, "sideButtonLayout", + StrId::STR_CAT_CONTROLS), SettingInfo::Toggle(StrId::STR_FRONT_BTN_FOLLOW_ORIENTATION, &CrossPointSettings::frontButtonFollowOrientation, "frontButtonFollowOrientation", StrId::STR_CAT_CONTROLS), SettingInfo::Enum(StrId::STR_LONG_PRESS_BEHAVIOR, &CrossPointSettings::longPressButtonBehavior, From 4ee406897bbde963b539dc2168e8ac536fefa072 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 25 May 2026 15:03:01 -0700 Subject: [PATCH 3/3] feat: tiled grayscale rendering to drop the storeBwBuffer peak (#2106) Tiled grayscale rendering to drop the storeBwBuffer peak largest contiguous free block (the value that actually drives OOM on the C3) from ~114 KB to ~82-90 KB. This renders each grayscale plane band-by-band into a small (~8 KB) scratch and streams each band straight to controller RAM (community-sdk writeGrayscalePlaneStrip), leaving the BW framebuffer intact. No save, no restore; controller RAM is re-synced for the next differential turn directly from the live framebuffer. Three writers honor the active band target so per-band re-rendering stays cheap and correct: - drawPixel (text) redirects writes to the band scratch and clips to it. - renderCharImpl skips glyphs whose physical y-extent is outside the band before the bitmap decode (glyphIntersectsStrip), so the per-band re-render doesn't pay N x glyph decode. - DirectPixelWriter (images) writes the band scratch via getWriteTarget instead of the framebuffer. Without this, image pixels wrote the live BW frame directly and cleanup re-synced that corruption, leaving thin outlines after navigating away from an image. Controller specifics live in the SDK (X4 setRamArea windowing, X3 PTL); the reader checks supportsStripGrayscale() and is otherwise controller-agnostic. Measured on hardware (X4 and X3, text and images, visually correct): - Grayscale scratch ~8 KB vs ~50 KB save; largest contiguous free block held at full size during grayscale instead of dropping ~25-32 KB. - X4 text page about +25 ms/page; X3 page time is dominated by its intrinsic grayscale refresh, not tiling. Depends on community-sdk #13 (the writeGrayscalePlaneStrip API). The submodule bump here points at that branch, so until #13 merges the submodule won't resolve from upstream and CI will fail there; keeping this a draft until then. Will rebase onto master and re-point the submodule to the merged SDK commit once #13 lands. Did you use AI tools to help write this code? partial --- lib/Epub/Epub/converters/DirectPixelWriter.h | 17 ++- lib/GfxRenderer/GfxRenderer.cpp | 81 ++++++++++- lib/GfxRenderer/GfxRenderer.h | 43 ++++++ lib/hal/HalDisplay.cpp | 7 + lib/hal/HalDisplay.h | 6 + open-x4-sdk | 2 +- src/activities/reader/EpubReaderActivity.cpp | 142 +++++++++++++------ 7 files changed, 249 insertions(+), 49 deletions(-) diff --git a/lib/Epub/Epub/converters/DirectPixelWriter.h b/lib/Epub/Epub/converters/DirectPixelWriter.h index bc66c2f78e..25cb57558e 100644 --- a/lib/Epub/Epub/converters/DirectPixelWriter.h +++ b/lib/Epub/Epub/converters/DirectPixelWriter.h @@ -16,6 +16,12 @@ struct DirectPixelWriter { uint8_t* fb; GfxRenderer::RenderMode mode; uint16_t displayWidthBytes; // Runtime framebuffer stride (X4: 100, X3: 99) + // Active write target: for tiled grayscale, fb is the band scratch, originY is + // the band's top physical row, and clipRows is the band height. Off-band + // pixels are dropped. With no strip active these collapse to the full frame + // (originY 0, clipRows panelHeight) so the clip doubles as a bounds guard. + int originY; + int clipRows; // Orientation is collapsed into a linear transform: // phyX = phyXBase + x * phyXStepX + y * phyXStepY @@ -28,7 +34,9 @@ struct DirectPixelWriter { int rowPhyXBase, rowPhyYBase; void init(GfxRenderer& renderer) { - fb = renderer.getFrameBuffer(); + fb = renderer.getWriteTarget(); + originY = renderer.getWriteOriginY(); + clipRows = renderer.getWriteRows(); mode = renderer.getRenderMode(); displayWidthBytes = renderer.getDisplayWidthBytes(); @@ -120,7 +128,12 @@ struct DirectPixelWriter { const int phyX = rowPhyXBase + logicalX * phyXStepX; const int phyY = rowPhyYBase + logicalX * phyYStepX; - const uint16_t byteIndex = phyY * displayWidthBytes + (phyX >> 3); + // Band-local row. The unsigned compare drops both off-band pixels (strip + // mode) and any out-of-frame row (full-frame mode) in one branch. + const int sy = phyY - originY; + if (static_cast(sy) >= static_cast(clipRows)) return; + + const uint16_t byteIndex = static_cast(sy * displayWidthBytes + (phyX >> 3)); const uint8_t bitMask = 1 << (7 - (phyX & 7)); if (state) { diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index a294531a3d..d6859cc300 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -148,6 +148,23 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode const int left = glyph->left; const int top = glyph->top; + // Tiled-grayscale band culling: if this glyph's physical y-extent is entirely + // outside the active strip, skip it before the expensive bitmap decode. This + // is what makes per-band re-rendering cheap. No-op outside strip mode. + if constexpr (rotation == TextRotation::Rotated90CW) { + const int ob = cursorX + fontData->ascender - top; + const int ib = cursorY - left; + if (!renderer.glyphIntersectsStrip(ob, ib - (width - 1), ob + height - 1, ib)) { + return; + } + } else { + const int gx0 = cursorX + left; + const int gy0 = cursorY - top; + if (!renderer.glyphIntersectsStrip(gx0, gy0, gx0 + width - 1, gy0 + height - 1)) { + return; + } + } + const uint8_t* bitmap = renderer.getGlyphBitmap(fontData, glyph); if (bitmap != nullptr) { @@ -238,14 +255,26 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { return; } + // Tiled grayscale: redirect writes to the strip scratch and clip to the + // current band. Single predictable branch on the hot per-pixel path. + uint8_t* target = frameBuffer; + uint32_t rowY = static_cast(phyY); + if (_stripActive) { + if (phyY < _stripY0 || phyY >= _stripY0 + _stripRows) { + return; // pixel outside the band currently being rendered + } + target = _stripBuf; + rowY = static_cast(phyY - _stripY0); + } + // Calculate byte position and bit position - const uint32_t byteIndex = static_cast(phyY) * panelWidthBytes + (phyX / 8); + const uint32_t byteIndex = rowY * panelWidthBytes + (phyX / 8); const uint8_t bitPosition = 7 - (phyX % 8); // MSB first if (state) { - frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit + target[byteIndex] &= ~(1 << bitPosition); // Clear bit } else { - frameBuffer[byteIndex] |= 1 << bitPosition; // Set bit + target[byteIndex] |= 1 << bitPosition; // Set bit } } @@ -964,9 +993,47 @@ static unsigned long start_ms = 0; void GfxRenderer::clearScreen(const uint8_t color) const { start_ms = millis(); + if (_stripActive) { + // Clear only the active band's scratch, not the shared framebuffer. + memset(_stripBuf, color, static_cast(panelWidthBytes) * _stripRows); + return; + } display.clearScreen(color); } +void GfxRenderer::beginStripTarget(uint8_t* scratch, int stripY0, int stripRows) const { + // Band is caller-guaranteed in-bounds (the reader's grayscale loop computes + // it); assert catches future misuse in debug before it mis-renders or wraps + // the downstream uint16_t cast in writeGrayscalePlaneStrip. + assert(scratch != nullptr && stripRows > 0 && stripY0 >= 0 && stripY0 <= static_cast(panelHeight) - stripRows); + _stripBuf = scratch; + _stripY0 = stripY0; + _stripRows = stripRows; + _stripActive = true; +} + +void GfxRenderer::endStripTarget() const { + _stripActive = false; + _stripBuf = nullptr; + _stripY0 = 0; + _stripRows = 0; +} + +bool GfxRenderer::glyphIntersectsStrip(int x0, int y0, int x1, int y1) const { + if (!_stripActive) { + return true; + } + // Rotate the two opposite bbox corners to physical coords. For 90-degree + // orientations the physical bbox stays axis-aligned, so min/max of the two + // rotated corners' Y bounds the glyph's physical y-extent. + int ax, ay, bx, by; + rotateCoordinates(orientation, x0, y0, &ax, &ay, panelWidth, panelHeight); + rotateCoordinates(orientation, x1, y1, &bx, &by, panelWidth, panelHeight); + const int minY = ay < by ? ay : by; + const int maxY = ay > by ? ay : by; + return !(maxY < _stripY0 || minY >= _stripY0 + _stripRows); +} + void GfxRenderer::invertScreen() const { for (uint32_t i = 0; i < frameBufferSize; i++) { frameBuffer[i] = ~frameBuffer[i]; @@ -1368,6 +1435,14 @@ void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuff void GfxRenderer::displayGrayBuffer() const { display.displayGrayBuffer(fadingFix); } +void GfxRenderer::writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* scratch, int yStart, int numRows) const { + // Guard the uint16_t casts below: a negative would wrap to a huge length. + assert(yStart >= 0 && numRows > 0 && yStart <= static_cast(panelHeight) - numRows); + display.writeGrayscalePlaneStrip(lsbPlane, scratch, static_cast(yStart), static_cast(numRows)); +} + +bool GfxRenderer::supportsStripGrayscale() const { return display.supportsStripGrayscale(); } + void GfxRenderer::freeBwBufferChunks() { for (auto& bwBufferChunk : bwBufferChunks) { if (bwBufferChunk) { diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index a97b2b9f85..78f1092296 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -54,6 +54,18 @@ class GfxRenderer { // as before, concentrated in a single pointer instead of four fields. mutable FontCacheManager* fontCacheManager_ = nullptr; + // Tiled grayscale strip target. When active, drawPixel()/clearScreen() + // operate on a caller-owned scratch holding one horizontal band of physical + // rows [_stripY0, _stripY0 + _stripRows) (panelWidthBytes wide) instead of + // the shared framebuffer, clipping pixels outside the band. Lets grayscale + // planes render band-by-band straight to the controller without destroying + // the BW framebuffer (no storeBwBuffer). Mutable because the render path is + // const. See beginStripTarget()/endStripTarget(). + mutable uint8_t* _stripBuf = nullptr; + mutable int _stripY0 = 0; + mutable int _stripRows = 0; + mutable bool _stripActive = false; + void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState, EpdFontFamily::Style style) const; void freeBwBufferChunks(); @@ -114,6 +126,31 @@ class GfxRenderer { void clearScreen(uint8_t color = 0xFF) const; void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const; + // Tiled grayscale strip target. While active, drawPixel() and clearScreen() + // operate on `scratch` (panelWidthBytes * stripRows bytes, holding physical + // rows [stripY0, stripY0 + stripRows)) instead of the framebuffer; pixels + // whose physical row falls outside the band are clipped. The clip is applied + // after the orientation rotate, so it is orientation-agnostic. Used to render + // grayscale planes band-by-band without a full second buffer. + void beginStripTarget(uint8_t* scratch, int stripY0, int stripRows) const; + void endStripTarget() const; + + // Band culling for tiled grayscale. Takes a glyph bounding box in logical + // screen coords and returns false only when a strip is active AND the box's + // physical y-extent lies entirely outside the active band, letting callers + // skip an expensive bitmap decode. Returns true when no strip is active. + // Corners are rotated to physical, so it is orientation-aware. + bool glyphIntersectsStrip(int x0, int y0, int x1, int y1) const; + + // Active pixel-write target for raw writers (DirectPixelWriter) that bypass + // drawPixel for speed. When a strip target is active these return the band + // scratch plus its physical-row origin and extent; otherwise the full + // framebuffer ([0, panelHeight)). Writers subtract the origin and clip to the + // extent, so they honor tiled-grayscale banding without per-pixel method calls. + uint8_t* getWriteTarget() const { return _stripActive ? _stripBuf : frameBuffer; } + int getWriteOriginY() const { return _stripActive ? _stripY0 : 0; } + int getWriteRows() const { return _stripActive ? _stripRows : panelHeight; } + // Drawing void drawPixel(int x, int y, bool state = true) const; void drawLine(int x1, int y1, int x2, int y2, bool state = true) const; @@ -172,6 +209,12 @@ class GfxRenderer { void copyGrayscaleLsbBuffers() const; void copyGrayscaleMsbBuffers() const; void displayGrayBuffer() const; + + // Tiled grayscale (X4): stream one band of a plane straight to controller RAM + // from `scratch` (panelWidthBytes * numRows, physical rows [yStart, yStart+ + // numRows)), bypassing the framebuffer. supportsStripGrayscale() gates use. + void writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* scratch, int yStart, int numRows) const; + bool supportsStripGrayscale() const; bool storeBwBuffer(); // Returns true if buffer was stored successfully void restoreBwBuffer(); // Restore and free the stored buffer void cleanupGrayscaleWithFrameBuffer() const; diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index a528db9f42..11f08758fc 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -89,6 +89,13 @@ void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { einkDisplay. void HalDisplay::displayGrayBuffer(bool turnOffScreen) { einkDisplay.displayGrayBuffer(turnOffScreen); } +void HalDisplay::writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* rows, uint16_t yStart, uint16_t numRows) { + einkDisplay.writeGrayscalePlaneStrip(lsbPlane ? EInkDisplay::GRAY_PLANE_LSB : EInkDisplay::GRAY_PLANE_MSB, rows, + yStart, numRows); +} + +bool HalDisplay::supportsStripGrayscale() const { return einkDisplay.supportsStripGrayscale(); } + uint16_t HalDisplay::getDisplayWidth() const { return einkDisplay.getDisplayWidth(); } uint16_t HalDisplay::getDisplayHeight() const { return einkDisplay.getDisplayHeight(); } diff --git a/lib/hal/HalDisplay.h b/lib/hal/HalDisplay.h index 3d43126a27..628296cb4e 100644 --- a/lib/hal/HalDisplay.h +++ b/lib/hal/HalDisplay.h @@ -54,6 +54,12 @@ class HalDisplay { void displayGrayBuffer(bool turnOffScreen = false); + // Tiled grayscale: stream one band of a plane (lsbPlane selects LSB/MSB RAM) + // straight to the controller; supportsStripGrayscale() gates the path. See + // EInkDisplay::writeGrayscalePlaneStrip. + void writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* rows, uint16_t yStart, uint16_t numRows); + bool supportsStripGrayscale() const; + // Runtime geometry passthrough uint16_t getDisplayWidth() const; uint16_t getDisplayHeight() const; diff --git a/open-x4-sdk b/open-x4-sdk index ff444b0f2c..ee4c58ddfe 160000 --- a/open-x4-sdk +++ b/open-x4-sdk @@ -1 +1 @@ -Subproject commit ff444b0f2c9211341e8a4a6c9d8d9c6bac03f3e8 +Subproject commit ee4c58ddfe9fac7d0f8f657bf83768db79c8a96c diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index f1bde915a1..d36afc5964 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -909,50 +910,105 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or } const auto tDisplay = millis(); - // Save bw buffer to reset buffer state after grayscale data sync - renderer.storeBwBuffer(); - const auto tBwStore = millis(); - - // grayscale rendering - // TODO: Only do this if font supports it - if (SETTINGS.textAntiAliasing) { - renderer.clearScreen(0x00); - renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); - renderer.copyGrayscaleLsbBuffers(); - const auto tGrayLsb = millis(); - - // Render and copy to MSB buffer - renderer.clearScreen(0x00); - renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); - renderer.copyGrayscaleMsbBuffers(); - const auto tGrayMsb = millis(); - - // display grayscale part - renderer.displayGrayBuffer(); - const auto tGrayDisplay = millis(); - renderer.setRenderMode(GfxRenderer::BW); - // restore the bw data - renderer.restoreBwBuffer(); - const auto tBwRestore = millis(); - - const auto tEnd = millis(); - LOG_DBG("ERS", - "Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums " - "gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums", - tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore, - tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0); + // Tiled grayscale: render each plane band-by-band into a small scratch and + // stream straight to the controller, leaving the BW framebuffer intact so no + // full-frame storeBwBuffer is needed; controller RAM is re-synced from the + // live framebuffer afterward. The page is re-rendered ceil(H/STRIP_ROWS) times + // per plane, but renderCharImpl culls out-of-band glyphs before decode so the + // cost stays close to one render. Both text (drawPixel) and images + // (DirectPixelWriter) honor the active strip target. + if (SETTINGS.textAntiAliasing && renderer.supportsStripGrayscale()) { + constexpr int STRIP_ROWS = 80; + const int gh = renderer.getDisplayHeight(); + const int gwBytes = renderer.getDisplayWidthBytes(); + + auto scratch = makeUniqueNoThrow(static_cast(gwBytes) * STRIP_ROWS); + if (!scratch) { + LOG_ERR("ERS", "OOM: grayscale strip scratch (%d bytes); skipping AA this page", gwBytes * STRIP_ROWS); + } else { + // Bands may be streamed in any order: X4 windows each via setRamArea, X3 + // via PTL. + renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); + for (int y = 0; y < gh; y += STRIP_ROWS) { + const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS; + renderer.beginStripTarget(scratch.get(), y, rows); + renderer.clearScreen(0x00); + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderer.endStripTarget(); + renderer.writeGrayscalePlaneStrip(true, scratch.get(), y, rows); + } + const auto tGrayLsb = millis(); + + // MSB plane. + renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); + for (int y = 0; y < gh; y += STRIP_ROWS) { + const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS; + renderer.beginStripTarget(scratch.get(), y, rows); + renderer.clearScreen(0x00); + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderer.endStripTarget(); + renderer.writeGrayscalePlaneStrip(false, scratch.get(), y, rows); + } + const auto tGrayMsb = millis(); + + renderer.setRenderMode(GfxRenderer::BW); + renderer.displayGrayBuffer(); + const auto tGrayDisplay = millis(); + + // BW framebuffer is intact; re-sync controller RAM for the next + // differential page turn directly from it. + renderer.cleanupGrayscaleWithFrameBuffer(); + const auto tCleanup = millis(); + + const auto tEnd = millis(); + LOG_DBG("ERS", + "Page render (tiled): prewarm=%lums bw_render=%lums display=%lums gray_lsb=%lums " + "gray_msb=%lums gray_display=%lums cleanup=%lums total=%lums", + tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayLsb - tDisplay, tGrayMsb - tGrayLsb, + tGrayDisplay - tGrayMsb, tCleanup - tGrayDisplay, tEnd - t0); + } } else { - // restore the bw data - renderer.restoreBwBuffer(); - const auto tBwRestore = millis(); - - const auto tEnd = millis(); - LOG_DBG("ERS", - "Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums", - tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tBwRestore - tBwStore, - tEnd - t0); + // Fallback path for a controller without strip support. grayscale rendering + // TODO: Only do this if font supports it + if (SETTINGS.textAntiAliasing) { + // Save the BW frame before the grayscale passes overwrite it, restore + // after. Only needed when grayscale actually renders. + renderer.storeBwBuffer(); + const auto tBwStore = millis(); + + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderer.copyGrayscaleLsbBuffers(); + const auto tGrayLsb = millis(); + + // Render and copy to MSB buffer + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderer.copyGrayscaleMsbBuffers(); + const auto tGrayMsb = millis(); + + // display grayscale part + renderer.displayGrayBuffer(); + const auto tGrayDisplay = millis(); + renderer.setRenderMode(GfxRenderer::BW); + renderer.restoreBwBuffer(); + const auto tBwRestore = millis(); + + const auto tEnd = millis(); + LOG_DBG("ERS", + "Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums " + "gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums", + tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore, + tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0); + } else { + // No anti-aliasing: BW frame already displayed above, no grayscale to + // render, so no save/restore. + const auto tEnd = millis(); + LOG_DBG("ERS", "Page render: prewarm=%lums bw_render=%lums display=%lums total=%lums", tPrewarm - t0, + tBwRender - tPrewarm, tDisplay - tBwRender, tEnd - t0); + } } }