## 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 >**_
## Summary
* **What is the goal of this PR?**
* This PR ports over Crossink's handling of large EPUBs that helps
prevent crashes during open after the book metadata cache is built.
* **What changes are included?**
* Removes the post-indexing ZIP-wide CSS discovery pass that built an
in-memory map of every ZIP entry.
* Reuses `content.opf` parsing to collect declared CSS files without
writing spine entries again.
* Temporarily releases the loaded book metadata cache while rebuilding
CSS for cached books.
* Parses CSS before reloading `book.bin` after a fresh cache build,
leaving more heap available during CSS rule parsing.
## Additional Context
* User reported their EPUB opening fine on Crossink but would crash on
Crosspoint. Verified this claim on my own devices.
* The crash this addresses happened after `book.bin` was successfully
built, when CSS discovery allocated a large `unordered_map` for ~3k EPUB
ZIP entries.
* Tradeoff: CSS files not declared in `content.opf` are no longer
discovered by scanning the full ZIP. This avoids the high-risk memory
allocation but improperly formatted EPUBs (ones that don't declare their
CSS styles in `content.opf` will render without styling and fallback to
inline styles.
* User provided epub that was crashing prior to this change:
https://www.mediafire.com/file/g57ea4mj13iunvh/Quang+%C3%82m+Chi+Ngo%E1%BA%A1i+-+Nh%C4%A9+C%C4%83n.epub/file
---
### 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 >**_
## Summary
* Fix the issue described in #2176 where bookmark percentages are always
0%
* Also fixed the page number rendering to +1 because they are 0 index
based, but should be rendered starting from 1
---
### AI Usage
Did you use AI tools to help write this code? _**< NO >**_
## Summary
* **What is the goal of this PR?**
Mainly fixes#2196, but also allows users to set a sleep time of 1~30
min or to never sleep.
* **What changes are included?**
1. Revert changes from #1948 and #2137 (Sorry @Uri-Tauber)
2. Cherrypick changes from
https://github.com/uxjulia/CrossInk/commit/0cb91c3dd1c63f0559d4392fadc4a8340af5bdde
and
https://github.com/uxjulia/CrossInk/commit/4e6bd634b6ede51ff1a7107ba3e8efcd152bc6ee
for a timeout interval picker UI, the custom sleep time feature, and
migrate the existing setting in v1.3.0, `sleepTimeout`, to
`sleepTimeoutMinutes` (Thanks! @uxjulia)
3. Add a "Never" at the far right of the timeout interval picker
---
### 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: Julia <julia@uxj.io>
## Summary
* **What is the goal of this PR?**
Improve the X3 clock UTC offset picker so it is clearer which part of
the offset will change when the user presses up/down.
* **What changes are included?**
Replaces the single underlined `UTC +/- H:MM` string with separate
editable fields for the sign, hour, and minutes.
The editable fields are spaced out, outlined, and the currently selected
field is highlighted with a light gray background.
## Additional Context
* This is a render-only UI change in `ClockOffsetActivity`; it does not
change how UTC offsets are stored, clamped, saved, or applied.
* The selected field still cycles with Confirm, and up/down still
adjusts the active sign/hour/minute value.
Screenshot of **current design** showing the sign (+/-) highlighted:
<img width="264" height="396" alt="old utc offset"
src="https://github.com/user-attachments/assets/c88d69ea-089c-47da-b5b3-ed0ebda5b112"
/>
Screenshot of **new design** showing the sign (+/-) highlighted:
<img width="264" height="396" alt="utc offset fix"
src="https://github.com/user-attachments/assets/f51da8b3-9aab-4f48-a9d1-21c828c01499"
/>
---
### 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 >**_
Chapter selection previously only used the spine index, causing
navigation to always land on page 0 of the spine item. Sub-chapters that
share a spine file but differ by anchor (e.g. `chapter.xhtml#sec2`) were
silently ignored. Now the TOC anchor is passed through `ChapterResult`
and applied via the existing `pendingAnchor` mechanism.
## Summary
* This PR implements navigation to sub-chapters which didn't work
correctly previously. If a sub-chapter of the current top-level chapter
was selected, nothing happened. If a sub-chapter of another top-level
chapter was selected, reader switched to the beginning of the top-level
chapter.
* In addition, chapters now always start from a new page. This fixes
anchor to page calculation for the cases when the actual chapter content
doesn't fit on the page where the corresponding ToC anchor was found.
## Additional Context
* I might misuse `pendingAnchor` here which was previously used for
footnote navigation, please double check. I'm open to suggestions for
improvements.
* Note that the chapter selected by default when
`EpubReaderChapterSelectionActivity` opens is still wrong. I'm going to
fix this separately. This PR addresses only navigation to the selected
chapter.
* I tested this PR on my X4 and verified that navigation to a different
sub-chapter works correctly, both inside and outside the current spine.
* Some of the changes were borrowed from
https://github.com/crosspoint-reader/crosspoint-reader/pull/1455
---
### 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: Claude Sonnet 4.6 <noreply@anthropic.com>
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**_
## Summary
* **What is the goal of this PR?** fixes#2132.
---
### 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 >**_
SdFat's SdSpiCard tracks SPI bus state with an unsynchronized
m_spiActive bool. When two tasks call into SdFat concurrently they can
confuse that state machine, ending with one task calling
SPIClass::endTransaction() against a paramLock the other task holds.
That trips FreeRTOS's xTaskPriorityDisinherit assert (tasks.c:5156,
pxTCB == pxCurrentTCBs[0]) and panics the system.
HalStorage already serialized every explicit method call via
storageMutex, but HalFile's destructor was `= default`, which let the
underlying SdFat FsFile destructor run close() outside any lock
(DESTRUCTOR_CLOSES_FILE=1). Any task that destructed a HalFile while
another task was mid-SD-op would race the unsynchronized state.
Move the locking discipline into HalFile::Impl::~Impl: an explicit
close() under StorageLock, then the FsFile member destructor's redundant
close() is a no-op. HalFile's special members can stay = default.
Switch storageMutex to xSemaphoreCreateRecursiveMutex so openFileForRead
and openFileForWrite can hold the lock while assigning to a HalFile&
out-param whose prior Impl needs locked teardown. Priority inheritance
still applies to recursive mutexes.
Also documented the no-bypass rule in CLAUDE.md: never call SdFat /
SdSpiCard / FsBaseFile / SDCardManager directly, never define
HAL_STORAGE_IMPL outside HalStorage.cpp.
Addresses my admittedly synthetic repro for #2047
Did you use AI tools to help write this code? partial
With both OtaUpdater and HttpDownloader on esp_http_client
(https://github.com/crosspoint-reader/crosspoint-reader/pull/2074,
https://github.com/crosspoint-reader/crosspoint-reader/pull/2075),
checkForUpdate no longer needs its own client and event handler to fetch
the release JSON. It streams the response straight into
ReleaseJsonParser through a new HttpDownloader::fetchUrl(url,
DataCallback) overload, dropping the duplicate esp_http_client setup,
the HTTP_EVENT_ON_DATA handler, and the totalBytesReceived file global.
DataCallback hands body chunks to a callback without buffering. The
naive alternative, collecting the ~32KB JSON into a std::string, aborts
under -fno-exceptions: the growing allocation collides with the TLS
session's heap mid-fetch and operator new calls abort().
The OTA install path stays on esp_https_ota (flash-write streaming),
which has no HttpDownloader equivalent.
HttpDownloader.h must precede the lwip (esp_http_client) headers in
OtaUpdater.cpp, or Arduino/SdFat macros collide with lwip.
Did you use AI tools to help write this code? partial