## Summary
* **What is the goal of this PR?**
Replace the pixel-by-pixel `fillRectDither` implementation with a new
byte-aligned `fillRectImpl` that eliminates per-pixel
`rotateCoordinates` calls and Read-Modify-Write bitwise loops, yielding
a significant rendering speedup on ESP32 E-ink framebuffers.
Ported from @rhythmerc's crosspoint-reader fork commit 27ea625.
* **What changes are included?**
- **`GfxRenderer.cpp` — `fillRectDither` refactor:** The existing
`if/else if` chain is replaced with a `switch` statement that delegates
each `Color` case to the new `fillRectImpl<Color>()` template,
eliminating runtime branching.
- **`GfxRenderer.cpp` — new `fillRectImpl<C>()` template:** Core of the
optimization. Key behaviors:
- Clips the rectangle in logical space upfront.
- Rotates only **2 opposing corner points** (top-left and bottom-right)
into physical framebuffer space instead of rotating every pixel
individually.
- Derives physical-space `byteStart`/`byteEnd` and precomputes
`headMask` / `tailMask` for MSB-first partial-byte boundaries,
performing RMW only on the edge bytes.
- **Solid fills (`Black` / `White`):** Uses `memset` for all interior
full-byte runs per row — no per-pixel writes.
- **Dithered fills (`LightGray` / `DarkGray`):** Precomputes both parity
variants of `blackMask` (even/odd `py`) **outside** the row loop,
eliminating the previously re-evaluated 8-bit construction loop on every
physical row. Interior full bytes are then written with a single
`memset(whiteMask)`.
- Uses `if constexpr` throughout to dispatch on `Color` at compile time,
generating zero runtime branches per template instantiation.
- **`GfxRenderer.h`:** Declares the new private `fillRectImpl<Color>()`
template method with an explanatory doc-comment.
- **Explicit template instantiations** added for all four active `Color`
variants (`Black`, `White`, `LightGray`, `DarkGray`).
## Additional Context
* **Performance:** The primary motivation is ESP32 E-ink framebuffer
performance. The old path called `rotateCoordinates` and did a full RMW
for every single pixel in the rectangle. The new path calls
`rotateCoordinates` exactly **twice** per fill regardless of rectangle
size, then operates at byte granularity — a complexity reduction from
O(W×H) coordinate transforms to O(1).
* **Dither correctness:** The `blackMask` precomputation relies on the
dither pattern having period 2 in both logical X and Y, which makes the
per-row byte pattern repeat with period 2 in `py`. Reviewers should
verify the `lxBase`/`lyBase` derivations for all four orientations
(`Portrait`, `PortraitInverted`, `LandscapeClockwise`,
`LandscapeCounterClockwise`) match the inverse of `rotateCoordinates`.
* **Edge case — single-byte rows:** When `byteStart == byteEnd`, the
head and tail masks are ANDed together into a single `rectMask` to avoid
double-masking the same byte. This path should be tested with narrow
rectangles (width < 8px).
* **No behavioral change for `Color::Clear`:** The `Clear` case exits
early via `if constexpr` and is a no-op, matching the original behavior.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
---------
Co-authored-by: Ryan Mercado <rmercado@firstdollar.com>
(#2334)
Introduces a new display method that prepares the framebuffer as a base
frame for grayscale overlays. On X3 panels, this uses the OEM
differential base waveform (AA-pre-BW) without forcing a resync. Other
panels fall back to normal display with configurable refresh mode.
Dependent upon matching SDK commit to work
## Summary
This PR optimizes the text rendering process by skipping underline style
calculations and measurements during the initial font cache scanning
phase. This prevents excessive and unnecessary SD card reads on pages
with heavy use of underlines (e.g., Table of Contents pages).
### **The Problem**
During the first rendering pass (the font cache scan pass used to
collect text for prewarming), `GfxRenderer::drawText()` early-returns
after recording text as expected. However, `TextBlock::render()`
continued past this point to execute the underline decoration logic.
Because underline calculation calls `getTextWidth()` and
`getTextAdvanceX()`, it triggered immediate glyph lookups via
`EpdFont::getGlyph()`. Since the SD card font had not been prewarmed yet
at this stage, the lookups fell back to the `glyphMissHandler`,
resulting in hundreds of individual, slow SD card reads into a limited
8-slot ring buffer.
### **The Fix**
1. **Exposed Scan State:** Added `GfxRenderer::isFontCacheScanning()` to
safely check if the font cache manager is currently in
text-collection/scan mode.
2. **Bypassed Underline Logic:** Modified `TextBlock::render()` to check
this state and skip underline measurement and drawing entirely while
scanning is active.
> [!NOTE]
> The text itself is still properly captured for prewarming via
`drawText()`. The underlines will be safely and efficiently calculated
and drawn during the actual render pass after the fonts have been
completely prewarmed.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
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
## Summary
Minor cleanup flagged by newer gcc:
- Removed unused variables
- Removed unimplemented function declaration
- `static` -> `inline` to avoid per-TU duplication
---
### AI Usage
Did you use AI tools to help write this code? _**NO**_
On-device repro showed the cover snapshot pinning ~52KB of contiguous
heap (cloning the full 48KB framebuffer with malloc overhead). MaxAlloc
on Home was 61KB; nothing was leaving headroom for HTTPS, which needs
30-50KB contiguous for the mbedTLS handshake.
Add region-aware framebuffer helpers to GfxRenderer that translate a
logical rect through rotateCoordinates and copy only the byte range that
contains the rotated rect. HomeActivity records the tile rect it passes
to drawRecentBookCover and caches only that subregion.
Measured on device (X3, Portrait):
Idle on Home | Free 102K -> 139K | MaxAlloc 61K -> 115K
Mid-EPUB-read | Free 81K -> 134K | MaxAlloc 70K -> 115K
Cover cache | ~52K -> ~16K (per allocation)
Works in all four orientations because the bounds helper samples the
four logical corners through the existing rotation, so the cached byte
range always covers the pixels the theme could have drawn into.
Savings will vary with theme, but should be significant across all.
## Summary
SD card font fixes:
- `TxtReaderActivity` needs to call `renderer.ensureSdCardFontReady` to
build the advance lookup table to support rendering with SD card fonts.
This revealed that `TxtReaderActivity` was inconsistently performing
layout with `getTextWidth`, when the renderer actually uses
`getTextAdvanceX`, which can lead to minor inconsistencies in alignment.
- Avoid allocating one big `allText` string in
`ParsedText::layoutAndExtractLines`. Instead, pass the vector of word
strings directly to `SdCardFont::buildAdvanceTable`, where the algorithm
just needs to iterate codepoints anyway.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
---------
Co-authored-by: Justin Mitchell <justin@jmitch.com>
## Summary
- Fixes#1928 by having the prewarm and advance table functions resolve
fallback styles
---
### AI Usage
Did you use AI tools to help write this code? YES - Codex
---------
Co-authored-by: Uri Tauber <uritaube@gmail.com>
Add a complete SD card font subsystem that enables users to install and
use custom fonts beyond the three built-in families. This combines the
back-end firmware support (#1327) with the font configuration, build
pipeline, CI distribution, and user-facing management UI (#1392).
Core font system:
- Custom .cpfont binary format (v4) with multi-style support (regular,
bold, italic, bold-italic) packed into a single file per size
- On-demand glyph loading from SD card with two-pass prewarm rendering
to bulk-read glyphs per page, achieving near-flash performance for
Latin text (~697ms vs ~681ms) and viable CJK rendering (~32% slower)
- Persistent advance cache for layout measurement without SD I/O
- Overflow ring buffer for glyph cache misses during rendering
- Memory-conscious design: only advance tables kept in RAM; glyph
bitmaps, kern tables, and ligatures loaded on demand from SD
Font management:
- On-device WiFi download from GitHub Releases with manifest-based
discovery, install/update detection, and progress UI
- Web interface font upload, listing, and deletion via /fonts page
- Manual SD card copy to /fonts/ or /.fonts/ directories
- Font selection integrated into Settings > Reader > Font Family
Build pipeline:
- Declarative YAML config (sd-fonts.yaml) as single source of truth
for the 17-family font library (serif, sans, mono, accessibility)
- Python converter (fontconvert_sdcard.py) for TTF/OTF to .cpfont with
FreeType rasterization, class-based kerning, and ligature extraction
- Parallel build orchestrator with variable font instance extraction
- CI workflow publishing versioned + stable releases to a dedicated
crosspoint-fonts repository with auto-incrementing revision tags
- Centralized version constants (cpfont_version.py) shared across
build tooling and CI, with firmware headers as manual sync points
Additional fixes:
- CJK characters no longer get hyphens inserted at line breaks
- Advance table eliminates 30+ second stalls during CJK section
indexing for paragraphs with >512 unique codepoints
Closes#930
Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Justin <itsthisjustin@users.noreply.github.com>
Co-authored-by: jpirnay <jens@pirnay.com>
Co-authored-by: mcrosson <kemonine@kemonine.info>
## Summary
Small simplification to node sorting algorithm in
GfxRenderer::fillPolygon.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
- Add new `roundedraff` theme.
- Fix sleep screen artifact where book cover showed grid lines in
`Cover` mode with `Crop`.
## Additional Context
## Key changes
- Added `src/components/themes/roundedraff/` theme implementation.
- Updated sleep cover rendering logic in:
- `src/activities/boot_sleep/SleepActivity.cpp`
- `src/activities/boot_sleep/SleepActivity.h`
- Improved cover generation/regeneration paths in:
- `lib/Epub/Epub.cpp`
- `lib/Epub/Epub.h`
- `lib/Txt/Txt.cpp`
- `lib/Txt/Txt.h`
## Verification
- Sleep screen set to Book cover
- Sleep screen cover mode set to Crop
- Confirmed grid artifacts no longer appear on cover sleep screen.
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
---------
Co-authored-by: CaptainFrito <yzq6x5zypy@privaterelay.appleid.com>
Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com>