Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98118d6e24 | ||
|
|
3cc4069faa | ||
|
|
a3b8897977 | ||
|
|
f7f1a6f8bf | ||
|
|
7cad3cfb0e | ||
|
|
e66de575a1 | ||
|
|
bae29fc6ba | ||
|
|
379f49d165 |
@@ -25,3 +25,9 @@ lib/EpdFont/scripts/output/
|
|||||||
# (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out.
|
# (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out.
|
||||||
.claude/*
|
.claude/*
|
||||||
!.claude/skills/
|
!.claude/skills/
|
||||||
|
/managed_components
|
||||||
|
/.dummy
|
||||||
|
/CMakeLists.txt
|
||||||
|
/dependencies.lock
|
||||||
|
/sdkconfig.default
|
||||||
|
/sdkconfig.defaults
|
||||||
|
|||||||
+1
-1
Submodule freeink-sdk updated: e7d336191b...4350bc7a0e
+44
-17
@@ -68,6 +68,22 @@ bool collectUniqueCodepoints(const char* text, uint32_t* codepoints, uint32_t& c
|
|||||||
const char* asCStr(const std::string& s) { return s.c_str(); }
|
const char* asCStr(const std::string& s) { return s.c_str(); }
|
||||||
const char* asCStr(const char* s) { return s; }
|
const char* asCStr(const char* s) { return s; }
|
||||||
|
|
||||||
|
// Keep-if-fits buffer reuse: only reallocate when the needed size exceeds the
|
||||||
|
// current capacity. Freeing + reallocating slightly different sizes every page
|
||||||
|
// turn punches non-coalescing holes in the heap (the freed block rarely fits the
|
||||||
|
// next page's need), eroding the largest contiguous block all session. With
|
||||||
|
// reuse, capacities converge on the book's max page after a few turns and page
|
||||||
|
// turns stop touching the allocator. Only three small instantiations exist
|
||||||
|
// (interval/glyph/byte arrays), so template bloat is negligible.
|
||||||
|
template <typename T, typename CapT>
|
||||||
|
bool ensureArrayCapacity(T*& buf, CapT& capacity, const uint32_t needed) {
|
||||||
|
if (buf && capacity >= needed) return true;
|
||||||
|
delete[] buf;
|
||||||
|
buf = new (std::nothrow) T[needed > 0 ? needed : 1];
|
||||||
|
capacity = buf ? static_cast<CapT>(needed) : 0;
|
||||||
|
return buf != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
SdCardFont::~SdCardFont() { freeAll(); }
|
SdCardFont::~SdCardFont() { freeAll(); }
|
||||||
@@ -83,6 +99,9 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
|||||||
s.miniBitmap = nullptr;
|
s.miniBitmap = nullptr;
|
||||||
s.miniIntervalCount = 0;
|
s.miniIntervalCount = 0;
|
||||||
s.miniGlyphCount = 0;
|
s.miniGlyphCount = 0;
|
||||||
|
s.miniIntervalCapacity = 0;
|
||||||
|
s.miniGlyphCapacity = 0;
|
||||||
|
s.miniBitmapCapacity = 0;
|
||||||
freeStyleMiniKern(s);
|
freeStyleMiniKern(s);
|
||||||
memset(&s.miniData, 0, sizeof(s.miniData));
|
memset(&s.miniData, 0, sizeof(s.miniData));
|
||||||
s.epdFont.data = &s.stubData;
|
s.epdFont.data = &s.stubData;
|
||||||
@@ -109,6 +128,9 @@ void SdCardFont::freeStyleMiniKern(PerStyle& s) {
|
|||||||
s.miniKernRightEntryCount = 0;
|
s.miniKernRightEntryCount = 0;
|
||||||
s.miniKernLeftClassCount = 0;
|
s.miniKernLeftClassCount = 0;
|
||||||
s.miniKernRightClassCount = 0;
|
s.miniKernRightClassCount = 0;
|
||||||
|
s.miniKernLeftCapacity = 0;
|
||||||
|
s.miniKernRightCapacity = 0;
|
||||||
|
s.miniKernMatrixCapacity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdCardFont::freeStyleAll(PerStyle& s) {
|
void SdCardFont::freeStyleAll(PerStyle& s) {
|
||||||
@@ -311,13 +333,13 @@ bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, ui
|
|||||||
if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++;
|
if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: allocate the three mini buffers. The matrix is <1KB in practice
|
// Step 4: size the three mini buffers (reused across pages when they fit; the
|
||||||
// (<30 × <30 × 1 byte) so fragmentation is a non-issue.
|
// per-page sizes vary by a few entries, which as free+realloc churn was punching
|
||||||
|
// non-coalescing holes in the heap every page turn).
|
||||||
const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight;
|
const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight;
|
||||||
s.miniKernLeftClasses = new (std::nothrow) EpdKernClassEntry[miniLeftCount];
|
if (!ensureArrayCapacity(s.miniKernLeftClasses, s.miniKernLeftCapacity, miniLeftCount) ||
|
||||||
s.miniKernRightClasses = new (std::nothrow) EpdKernClassEntry[miniRightCount];
|
!ensureArrayCapacity(s.miniKernRightClasses, s.miniKernRightCapacity, miniRightCount) ||
|
||||||
s.miniKernMatrix = new (std::nothrow) int8_t[matrixBytes];
|
!ensureArrayCapacity(s.miniKernMatrix, s.miniKernMatrixCapacity, matrixBytes)) {
|
||||||
if (!s.miniKernLeftClasses || !s.miniKernRightClasses || !s.miniKernMatrix) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u,
|
LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u,
|
||||||
matrixBytes);
|
matrixBytes);
|
||||||
freeStyleMiniKern(s);
|
freeStyleMiniKern(s);
|
||||||
@@ -793,12 +815,19 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
return missed;
|
return missed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build mini intervals from sorted codepoints
|
// Build mini intervals from sorted codepoints. Reset counts and fall back to the
|
||||||
freeStyleMiniData(s);
|
// stub until the rebuild completes, but KEEP the existing buffers (keep-if-fits
|
||||||
|
// reuse) — the free-and-realloc-per-page pattern here was a primary fragmenter.
|
||||||
|
s.miniIntervalCount = 0;
|
||||||
|
s.miniGlyphCount = 0;
|
||||||
|
s.miniKernLeftEntryCount = 0;
|
||||||
|
s.miniKernRightEntryCount = 0;
|
||||||
|
s.miniKernLeftClassCount = 0;
|
||||||
|
s.miniKernRightClassCount = 0;
|
||||||
|
memset(&s.miniData, 0, sizeof(s.miniData));
|
||||||
|
s.epdFont.data = &s.stubData;
|
||||||
|
|
||||||
uint32_t intervalCapacity = validCount;
|
if (!ensureArrayCapacity(s.miniIntervals, s.miniIntervalCapacity, validCount)) {
|
||||||
s.miniIntervals = new (std::nothrow) EpdUnicodeInterval[intervalCapacity];
|
|
||||||
if (!s.miniIntervals) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
@@ -816,15 +845,14 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate mini glyph array
|
// Mini glyph array (reused across pages when it fits)
|
||||||
s.miniGlyphCount = validCount;
|
if (!ensureArrayCapacity(s.miniGlyphs, s.miniGlyphCapacity, validCount)) {
|
||||||
s.miniGlyphs = new (std::nothrow) EpdGlyph[s.miniGlyphCount];
|
|
||||||
if (!s.miniGlyphs) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
freeStyleMiniData(s);
|
freeStyleMiniData(s);
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
}
|
}
|
||||||
|
s.miniGlyphCount = validCount;
|
||||||
|
|
||||||
// Build sorted read order for sequential I/O
|
// Build sorted read order for sequential I/O
|
||||||
uint32_t* readOrder = new (std::nothrow) uint32_t[validCount];
|
uint32_t* readOrder = new (std::nothrow) uint32_t[validCount];
|
||||||
@@ -891,8 +919,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
totalBitmapSize += s.miniGlyphs[i].dataLength;
|
totalBitmapSize += s.miniGlyphs[i].dataLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
s.miniBitmap = new (std::nothrow) uint8_t[totalBitmapSize > 0 ? totalBitmapSize : 1];
|
if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) {
|
||||||
if (!s.miniBitmap) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
||||||
delete[] readOrder;
|
delete[] readOrder;
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
|
|||||||
@@ -168,13 +168,22 @@ class SdCardFont {
|
|||||||
// Stub EpdFontData returned when not prewarmed
|
// Stub EpdFontData returned when not prewarmed
|
||||||
EpdFontData stubData{};
|
EpdFontData stubData{};
|
||||||
|
|
||||||
// Mini EpdFontData built during prewarm
|
// Mini EpdFontData built during prewarm. Buffers are kept-if-fits across pages
|
||||||
|
// (capacities below track allocated sizes): freeing and reallocating slightly
|
||||||
|
// different sizes on every page turn was a primary heap fragmenter — each page's
|
||||||
|
// freed hole rarely fit the next page's need, so maxAlloc eroded all session.
|
||||||
|
// After a few pages the capacities converge on the book's max and page turns
|
||||||
|
// stop allocating entirely. freeStyleMiniData() still releases everything (and
|
||||||
|
// zeroes capacities) for style eviction / font unload.
|
||||||
EpdFontData miniData{};
|
EpdFontData miniData{};
|
||||||
EpdUnicodeInterval* miniIntervals = nullptr;
|
EpdUnicodeInterval* miniIntervals = nullptr;
|
||||||
EpdGlyph* miniGlyphs = nullptr;
|
EpdGlyph* miniGlyphs = nullptr;
|
||||||
uint8_t* miniBitmap = nullptr;
|
uint8_t* miniBitmap = nullptr;
|
||||||
uint32_t miniIntervalCount = 0;
|
uint32_t miniIntervalCount = 0;
|
||||||
uint32_t miniGlyphCount = 0;
|
uint32_t miniGlyphCount = 0;
|
||||||
|
uint32_t miniIntervalCapacity = 0;
|
||||||
|
uint32_t miniGlyphCapacity = 0;
|
||||||
|
uint32_t miniBitmapCapacity = 0;
|
||||||
|
|
||||||
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
|
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
|
||||||
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
|
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
|
||||||
@@ -189,6 +198,10 @@ class SdCardFont {
|
|||||||
uint8_t miniKernLeftClassCount = 0;
|
uint8_t miniKernLeftClassCount = 0;
|
||||||
uint8_t miniKernRightClassCount = 0;
|
uint8_t miniKernRightClassCount = 0;
|
||||||
int8_t* miniKernMatrix = nullptr;
|
int8_t* miniKernMatrix = nullptr;
|
||||||
|
// Kept-if-fits capacities, same rationale as the mini glyph buffers above.
|
||||||
|
uint16_t miniKernLeftCapacity = 0;
|
||||||
|
uint16_t miniKernRightCapacity = 0;
|
||||||
|
uint32_t miniKernMatrixCapacity = 0;
|
||||||
|
|
||||||
// The EpdFont whose data pointer we manage
|
// The EpdFont whose data pointer we manage
|
||||||
EpdFont epdFont{&stubData};
|
EpdFont epdFont{&stubData};
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
#include "BookPages.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <limits>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Coarse seed used only before any section has an exact or live count; replaced
|
|
||||||
// by the calibrated average as soon as one is available.
|
|
||||||
constexpr double DEFAULT_BYTES_PER_PAGE = 2000.0;
|
|
||||||
// A section's serialized pageCount is a uint16_t, so no estimate needs to exceed this.
|
|
||||||
constexpr int MAX_SECTION_PAGES = std::numeric_limits<uint16_t>::max();
|
|
||||||
|
|
||||||
int clampToInt(const uint64_t v) {
|
|
||||||
return v > static_cast<uint64_t>(std::numeric_limits<int>::max()) ? std::numeric_limits<int>::max()
|
|
||||||
: static_cast<int>(v);
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
BookPagePosition computeBookPagePosition(const BookPageEntry* entries, const int sectionCount, const int spineIndex,
|
|
||||||
const int pageInSection, const int liveSectionPages) {
|
|
||||||
BookPagePosition pos;
|
|
||||||
if (!entries || sectionCount <= 0) {
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calibrate bytes-per-page from every section with an exact count (empty
|
|
||||||
// known-0 chapters excluded), plus the live estimate for spineIndex when it
|
|
||||||
// has no exact count yet.
|
|
||||||
uint64_t knownBytes = 0;
|
|
||||||
uint64_t knownPages = 0;
|
|
||||||
for (int i = 0; i < sectionCount; ++i) {
|
|
||||||
if (entries[i].pages > 0) {
|
|
||||||
knownBytes += entries[i].bytes;
|
|
||||||
knownPages += static_cast<uint32_t>(entries[i].pages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const bool useLive = spineIndex >= 0 && spineIndex < sectionCount && entries[spineIndex].pages < 0 &&
|
|
||||||
liveSectionPages > 0 && entries[spineIndex].bytes > 0;
|
|
||||||
if (useLive) {
|
|
||||||
knownBytes += entries[spineIndex].bytes;
|
|
||||||
knownPages += static_cast<uint32_t>(liveSectionPages);
|
|
||||||
}
|
|
||||||
const double bytesPerPage = (knownBytes > 0 && knownPages > 0)
|
|
||||||
? static_cast<double>(knownBytes) / static_cast<double>(knownPages)
|
|
||||||
: DEFAULT_BYTES_PER_PAGE;
|
|
||||||
|
|
||||||
uint64_t total = 0;
|
|
||||||
uint64_t before = 0;
|
|
||||||
bool exact = true;
|
|
||||||
for (int i = 0; i < sectionCount; ++i) {
|
|
||||||
uint32_t pages;
|
|
||||||
if (entries[i].pages >= 0) {
|
|
||||||
pages = static_cast<uint32_t>(entries[i].pages); // exact (0 = genuinely empty chapter)
|
|
||||||
} else if (i == spineIndex && liveSectionPages > 0) {
|
|
||||||
pages = static_cast<uint32_t>(std::min(liveSectionPages, MAX_SECTION_PAGES));
|
|
||||||
exact = false;
|
|
||||||
} else {
|
|
||||||
const double raw = std::floor(static_cast<double>(entries[i].bytes) / bytesPerPage + 0.5);
|
|
||||||
pages = static_cast<uint32_t>(std::clamp(raw, 1.0, static_cast<double>(MAX_SECTION_PAGES)));
|
|
||||||
exact = false;
|
|
||||||
}
|
|
||||||
total += pages;
|
|
||||||
if (i < spineIndex) {
|
|
||||||
before += pages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pos.totalPages = clampToInt(total);
|
|
||||||
pos.currentPage = clampToInt(before + static_cast<uint64_t>(std::max(0, pageInSection)) + 1);
|
|
||||||
pos.isEstimate = !exact;
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
// Whole-book page accounting for book-global "page X of Y".
|
|
||||||
//
|
|
||||||
// The finalized section cache files (sections/*.bin) are the single source of
|
|
||||||
// truth for exact per-section counts; the caller harvests them into an array of
|
|
||||||
// BookPageEntry (nothing is persisted separately). Sections without an exact
|
|
||||||
// count are estimated from their byte size, calibrated against the sections
|
|
||||||
// whose counts are known — so the total gets more accurate as more of the book
|
|
||||||
// is paginated, and becomes exact once every section is.
|
|
||||||
|
|
||||||
struct BookPageEntry {
|
|
||||||
uint32_t bytes = 0; // uncompressed XHTML size, for estimating unknown sections
|
|
||||||
int32_t pages = -1; // exact count from a finalized section cache; -1 = unknown (0 = empty chapter)
|
|
||||||
};
|
|
||||||
|
|
||||||
struct BookPagePosition {
|
|
||||||
int currentPage = 0;
|
|
||||||
int totalPages = 0;
|
|
||||||
bool isEstimate = true; // true until every section has an exact count
|
|
||||||
};
|
|
||||||
|
|
||||||
// Book-global position: currentPage = pages before spineIndex + pageInSection + 1.
|
|
||||||
// liveSectionPages is the in-progress build's estimate for spineIndex (see
|
|
||||||
// Section::estimatedTotalPages); it is used for that section when it has no exact
|
|
||||||
// count yet and folded into the bytes-per-page calibration. Pure function: no I/O.
|
|
||||||
BookPagePosition computeBookPagePosition(const BookPageEntry* entries, int sectionCount, int spineIndex,
|
|
||||||
int pageInSection, int liveSectionPages);
|
|
||||||
+26
-38
@@ -39,34 +39,8 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) +
|
|||||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
||||||
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
|
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
|
||||||
sizeof(uint32_t) + sizeof(uint32_t);
|
sizeof(uint32_t) + sizeof(uint32_t);
|
||||||
|
|
||||||
// Read the render-parameter block of the header (everything between the version
|
|
||||||
// byte and pageCount), in writeSectionFileHeader order. The file cursor must sit
|
|
||||||
// just past the version byte.
|
|
||||||
Section::RenderParams readHeaderRenderParams(HalFile& f) {
|
|
||||||
Section::RenderParams p;
|
|
||||||
serialization::readPod(f, p.fontId);
|
|
||||||
serialization::readPod(f, p.lineCompression);
|
|
||||||
serialization::readPod(f, p.extraParagraphSpacing);
|
|
||||||
serialization::readPod(f, p.paragraphAlignment);
|
|
||||||
serialization::readPod(f, p.viewportWidth);
|
|
||||||
serialization::readPod(f, p.viewportHeight);
|
|
||||||
serialization::readPod(f, p.hyphenationEnabled);
|
|
||||||
serialization::readPod(f, p.embeddedStyle);
|
|
||||||
serialization::readPod(f, p.imageRendering);
|
|
||||||
serialization::readPod(f, p.focusReadingEnabled);
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool Section::RenderParams::operator==(const RenderParams& o) const {
|
|
||||||
return fontId == o.fontId && lineCompression == o.lineCompression &&
|
|
||||||
extraParagraphSpacing == o.extraParagraphSpacing && paragraphAlignment == o.paragraphAlignment &&
|
|
||||||
viewportWidth == o.viewportWidth && viewportHeight == o.viewportHeight &&
|
|
||||||
hyphenationEnabled == o.hyphenationEnabled && embeddedStyle == o.embeddedStyle &&
|
|
||||||
imageRendering == o.imageRendering && focusReadingEnabled == o.focusReadingEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Out-of-line so the unique_ptr<ChapterHtmlSlimParser> in BuildContext can be
|
// Out-of-line so the unique_ptr<ChapterHtmlSlimParser> in BuildContext can be
|
||||||
// constructed/destroyed where the parser's full definition is visible.
|
// constructed/destroyed where the parser's full definition is visible.
|
||||||
Section::Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
Section::Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
||||||
@@ -159,11 +133,31 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
}
|
}
|
||||||
filePartial = (version == SECTION_FILE_PARTIAL_VERSION);
|
filePartial = (version == SECTION_FILE_PARTIAL_VERSION);
|
||||||
|
|
||||||
const RenderParams fileParams = readHeaderRenderParams(file);
|
int fileFontId;
|
||||||
const RenderParams params = {fontId, lineCompression, extraParagraphSpacing, paragraphAlignment,
|
uint16_t fileViewportWidth, fileViewportHeight;
|
||||||
viewportWidth, viewportHeight, hyphenationEnabled, embeddedStyle,
|
float fileLineCompression;
|
||||||
imageRendering, focusReadingEnabled};
|
bool fileExtraParagraphSpacing;
|
||||||
if (!(fileParams == params)) {
|
uint8_t fileParagraphAlignment;
|
||||||
|
bool fileHyphenationEnabled;
|
||||||
|
bool fileEmbeddedStyle;
|
||||||
|
uint8_t fileImageRendering;
|
||||||
|
bool fileFocusReadingEnabled;
|
||||||
|
serialization::readPod(file, fileFontId);
|
||||||
|
serialization::readPod(file, fileLineCompression);
|
||||||
|
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||||
|
serialization::readPod(file, fileParagraphAlignment);
|
||||||
|
serialization::readPod(file, fileViewportWidth);
|
||||||
|
serialization::readPod(file, fileViewportHeight);
|
||||||
|
serialization::readPod(file, fileHyphenationEnabled);
|
||||||
|
serialization::readPod(file, fileEmbeddedStyle);
|
||||||
|
serialization::readPod(file, fileImageRendering);
|
||||||
|
serialization::readPod(file, fileFocusReadingEnabled);
|
||||||
|
|
||||||
|
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||||
|
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||||
|
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||||
|
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
|
||||||
|
imageRendering != fileImageRendering || focusReadingEnabled != fileFocusReadingEnabled) {
|
||||||
file.close();
|
file.close();
|
||||||
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||||
clearCache();
|
clearCache();
|
||||||
@@ -760,7 +754,7 @@ std::string Section::getTextFromSectionFile() {
|
|||||||
return fullText;
|
return fullText;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint16_t> Section::getCachedPageCount(const RenderParams* mustMatch) const {
|
std::optional<uint16_t> Section::getCachedPageCount() const {
|
||||||
HalFile f;
|
HalFile f;
|
||||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -780,12 +774,6 @@ std::optional<uint16_t> Section::getCachedPageCount(const RenderParams* mustMatc
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A file cached under other render settings paginates differently; its count is
|
|
||||||
// only stale-valid for rough mapping, so reject it when the caller needs a match.
|
|
||||||
if (mustMatch && !(readHeaderRenderParams(f) == *mustMatch)) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
|
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
|
||||||
uint16_t count;
|
uint16_t count;
|
||||||
serialization::readPod(f, count);
|
serialization::readPod(f, count);
|
||||||
|
|||||||
+1
-21
@@ -78,23 +78,6 @@ class Section {
|
|||||||
std::unique_ptr<Page> loadPageDuringBuild(int page);
|
std::unique_ptr<Page> loadPageDuringBuild(int page);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// The render parameters that determine pagination, in section cache header order
|
|
||||||
// (see writeSectionFileHeader). A cached page count is only valid for one set.
|
|
||||||
struct RenderParams {
|
|
||||||
int fontId = 0;
|
|
||||||
float lineCompression = 0.0f;
|
|
||||||
bool extraParagraphSpacing = false;
|
|
||||||
uint8_t paragraphAlignment = 0;
|
|
||||||
uint16_t viewportWidth = 0;
|
|
||||||
uint16_t viewportHeight = 0;
|
|
||||||
bool hyphenationEnabled = false;
|
|
||||||
bool embeddedStyle = false;
|
|
||||||
uint8_t imageRendering = 0;
|
|
||||||
bool focusReadingEnabled = false;
|
|
||||||
|
|
||||||
bool operator==(const RenderParams& o) const;
|
|
||||||
};
|
|
||||||
|
|
||||||
uint16_t pageCount = 0;
|
uint16_t pageCount = 0;
|
||||||
int currentPage = 0;
|
int currentPage = 0;
|
||||||
|
|
||||||
@@ -161,10 +144,7 @@ class Section {
|
|||||||
std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
|
std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
|
||||||
|
|
||||||
// Get the page count from the section cache file without fully loading it.
|
// Get the page count from the section cache file without fully loading it.
|
||||||
// Finalized files only (a partial's count is just a build watermark). When
|
std::optional<uint16_t> getCachedPageCount() const;
|
||||||
// `mustMatch` is given, the header's render parameters must equal it, so a
|
|
||||||
// count cached under different settings is never trusted.
|
|
||||||
std::optional<uint16_t> getCachedPageCount(const RenderParams* mustMatch = nullptr) const;
|
|
||||||
|
|
||||||
// Look up the page number for a synthetic paragraph index from XPath p[N].
|
// Look up the page number for a synthetic paragraph index from XPath p[N].
|
||||||
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
|
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
|
||||||
|
|||||||
@@ -1451,6 +1451,20 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
|
|||||||
display.displayBuffer(refreshMode, fadingFix);
|
display.displayBuffer(refreshMode, fadingFix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::displayBufferAsync(const HalDisplay::RefreshMode refreshMode) const {
|
||||||
|
// The async path has no turn-off-screen hook, which the sunlight fading fix
|
||||||
|
// relies on; keep those users on the blocking path.
|
||||||
|
if (fadingFix) {
|
||||||
|
display.displayBuffer(refreshMode, fadingFix);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
display.displayBufferAsync(refreshMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::waitRefreshComplete() const { display.waitRefreshComplete(); }
|
||||||
|
|
||||||
|
bool GfxRenderer::supportsAsyncRefresh() const { return !fadingFix && display.supportsAsyncRefresh(); }
|
||||||
|
|
||||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||||
const EpdFontFamily::Style style) const {
|
const EpdFontFamily::Style style) const {
|
||||||
if (!text || maxWidth <= 0) return "";
|
if (!text || maxWidth <= 0) return "";
|
||||||
|
|||||||
@@ -135,6 +135,17 @@ class GfxRenderer {
|
|||||||
int getScreenWidth() const;
|
int getScreenWidth() const;
|
||||||
int getScreenHeight() const;
|
int getScreenHeight() const;
|
||||||
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||||
|
// Non-blocking refresh: starts the waveform and returns so CPU work (e.g.
|
||||||
|
// grayscale strip rendering) can overlap the panel's refresh time. The
|
||||||
|
// framebuffer must stay untouched until waitRefreshComplete(). Falls back to
|
||||||
|
// a blocking refresh when fadingFix is enabled or the panel lacks deferral
|
||||||
|
// support. See HalDisplay::displayBufferAsync for the baseline contract.
|
||||||
|
void displayBufferAsync(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||||
|
void waitRefreshComplete() const;
|
||||||
|
// True when displayBufferAsync() genuinely overlaps: panel defers and
|
||||||
|
// fadingFix isn't forcing the blocking path. Callers can skip overlap
|
||||||
|
// scaffolding (e.g. whole-plane grayscale buffers) when false.
|
||||||
|
bool supportsAsyncRefresh() const;
|
||||||
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
||||||
// void displayWindow(int x, int y, int width, int height) const;
|
// void displayWindow(int x, int y, int width, int height) const;
|
||||||
void invertScreen() const;
|
void invertScreen() const;
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ STR_OK_BUTTON: "OK"
|
|||||||
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
|
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
|
||||||
STR_FILTER_CONTRAST: "Contrast"
|
STR_FILTER_CONTRAST: "Contrast"
|
||||||
STR_CUSTOMISE_STATUS_BAR: "Customise Status Bar"
|
STR_CUSTOMISE_STATUS_BAR: "Customise Status Bar"
|
||||||
STR_CHAPTER_PAGE_COUNT: "Page Count"
|
STR_CHAPTER_PAGE_COUNT: "Chapter Page Count"
|
||||||
STR_BOOK_PROGRESS_PERCENTAGE: "Book Progress Percentage"
|
STR_BOOK_PROGRESS_PERCENTAGE: "Book Progress Percentage"
|
||||||
STR_PROGRESS_BAR: "Progress Bar"
|
STR_PROGRESS_BAR: "Progress Bar"
|
||||||
STR_PROGRESS_BAR_THICKNESS: "Progress Bar Thickness"
|
STR_PROGRESS_BAR_THICKNESS: "Progress Bar Thickness"
|
||||||
|
|||||||
@@ -22,7 +22,21 @@ constexpr char DEVICE_ID[] = "crosspoint-reader";
|
|||||||
// footprint is smaller than mbedTLS's old ~48KB peak, but keep a conservative
|
// footprint is smaller than mbedTLS's old ~48KB peak, but keep a conservative
|
||||||
// floor. Check both total free heap and largest contiguous block so fragmented
|
// floor. Check both total free heap and largest contiguous block so fragmented
|
||||||
// heap does not fall through into a failed TLS allocation path.
|
// heap does not fall through into a failed TLS allocation path.
|
||||||
constexpr uint32_t MIN_HEAP_FOR_TLS = 55000;
|
// MEMFIX-PORT: TLS heap gate; portable
|
||||||
|
// Field data (July 2026): launching sync from a reader session lands at
|
||||||
|
// 51.9-58.2 KB free / 42-53 KB maxAlloc after WiFi comes up. wolfSSL handles
|
||||||
|
// allocation failure by returning MEMORY_E (no abort under -fno-exceptions),
|
||||||
|
// so an optimistic attempt degrades to the same clean "sync failed" as the
|
||||||
|
// gate — the gate only needs to keep out states where a doomed handshake
|
||||||
|
// would waste tens of seconds, not guarantee success.
|
||||||
|
//
|
||||||
|
// Free and largest-block have separate requirements: with SP ECC
|
||||||
|
// (WOLFSSL_HAVE_SP_ECC) the handshake's crypto uses fixed 256-bit arrays, so
|
||||||
|
// the largest single TLS allocation is the ~17 KB wolfSSL record buffer, not
|
||||||
|
// a run of fast-math bignums. A handshake was measured succeeding inside a
|
||||||
|
// 43 KB largest block; requiring 50 KB contiguous refused syncs that fit.
|
||||||
|
constexpr uint32_t MIN_FREE_FOR_TLS = 50000;
|
||||||
|
constexpr uint32_t MIN_BLOCK_FOR_TLS = 20000;
|
||||||
|
|
||||||
// Apply the shared KOSync auth headers after begin(). x-auth-* is the native
|
// Apply the shared KOSync auth headers after begin(). x-auth-* is the native
|
||||||
// KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility.
|
// KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility.
|
||||||
@@ -39,9 +53,9 @@ void applyAuthHeaders(freeink::SecureHttpClient& http) {
|
|||||||
bool insufficientHeap() {
|
bool insufficientHeap() {
|
||||||
const uint32_t freeHeap = ESP.getFreeHeap();
|
const uint32_t freeHeap = ESP.getFreeHeap();
|
||||||
const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
|
const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
|
||||||
if (freeHeap < MIN_HEAP_FOR_TLS || maxAllocHeap < MIN_HEAP_FOR_TLS) {
|
if (freeHeap < MIN_FREE_FOR_TLS || maxAllocHeap < MIN_BLOCK_FOR_TLS) {
|
||||||
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free, %u max alloc (need %u)", freeHeap,
|
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u), %u max alloc (need %u)", freeHeap,
|
||||||
maxAllocHeap, MIN_HEAP_FOR_TLS);
|
MIN_FREE_FOR_TLS, maxAllocHeap, MIN_BLOCK_FOR_TLS);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -65,6 +65,18 @@ void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen)
|
|||||||
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
|
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HalDisplay::displayBufferAsync(HalDisplay::RefreshMode mode) {
|
||||||
|
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
|
||||||
|
einkDisplay.requestResync(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
einkDisplay.displayBufferAsyncNoShadow(convertRefreshMode(mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::waitRefreshComplete() { einkDisplay.waitRefreshComplete(); }
|
||||||
|
|
||||||
|
bool HalDisplay::supportsAsyncRefresh() const { return einkDisplay.supportsAsyncRefresh(); }
|
||||||
|
|
||||||
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
||||||
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
|
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
|
||||||
einkDisplay.requestResync(1);
|
einkDisplay.requestResync(1);
|
||||||
|
|||||||
@@ -39,6 +39,17 @@ class HalDisplay {
|
|||||||
bool fromProgmem = false) const;
|
bool fromProgmem = false) const;
|
||||||
|
|
||||||
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||||
|
// Non-blocking refresh (shadow-free): starts the panel waveform and returns
|
||||||
|
// while the panel refreshes on its own. The framebuffer must stay untouched
|
||||||
|
// until waitRefreshComplete(), and the caller must rebuild the differential
|
||||||
|
// baseline before the next differential update (the tiled grayscale cleanup
|
||||||
|
// does). Panels without deferral fall back to a blocking refresh.
|
||||||
|
void displayBufferAsync(RefreshMode mode = RefreshMode::FAST_REFRESH);
|
||||||
|
// Block until a pending deferred refresh completes (no-op when none is).
|
||||||
|
void waitRefreshComplete();
|
||||||
|
// True when displayBufferAsync() genuinely overlaps (panel driver defers);
|
||||||
|
// false where it falls back to a blocking refresh.
|
||||||
|
bool supportsAsyncRefresh() const;
|
||||||
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||||
|
|
||||||
// Power management
|
// Power management
|
||||||
|
|||||||
+53
-2
@@ -13,7 +13,10 @@ framework = arduino
|
|||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
upload_speed = 921600
|
upload_speed = 921600
|
||||||
check_tool = cppcheck
|
check_tool = cppcheck
|
||||||
check_flags = --enable=all --suppress=missingIncludeSystem --suppress=unusedFunction --suppress=unmatchedSuppression --suppress=*:*/.pio/* --inline-suppr
|
; missingInclude (project headers) is suppressed alongside missingIncludeSystem: on a
|
||||||
|
; fresh CI checkout cppcheck has no resolved include paths, so it reports every
|
||||||
|
; project header as missing (~400 information-level lines) and fails the job.
|
||||||
|
check_flags = --enable=all --suppress=missingIncludeSystem --suppress=missingInclude --suppress=unusedFunction --suppress=unmatchedSuppression --suppress=*:*/.pio/* --inline-suppr
|
||||||
check_skip_packages = yes
|
check_skip_packages = yes
|
||||||
|
|
||||||
board_upload.flash_size = 16MB
|
board_upload.flash_size = 16MB
|
||||||
@@ -42,7 +45,14 @@ build_flags =
|
|||||||
-DWOLFSSL_OPTIONS_H
|
-DWOLFSSL_OPTIONS_H
|
||||||
-DWOLFSSL_CLIENT_EXAMPLE
|
-DWOLFSSL_CLIENT_EXAMPLE
|
||||||
-DWOLFSSL_TLS13
|
-DWOLFSSL_TLS13
|
||||||
-DWOLFSSL_SP_RISCV32
|
# MEMFIX-PORT: single-precision ECC (sp_c32.c). Without it every P-256 operation
|
||||||
|
# (TLS 1.3 key_share keygen, ECDHE, ECDSA cert verify) runs on fast-math bignums
|
||||||
|
# that WOLFSSL_SMALL_STACK heap-allocates at FP_MAX_BITS size -- tens of KB of
|
||||||
|
# temporaries, which OOMs (MP_MEM) at the ~50KB free heap a reading session
|
||||||
|
# leaves. SP uses fixed 256-bit arrays: a few KB, and several times faster.
|
||||||
|
# SP_SMALL trades the large precomputed point tables for smaller flash.
|
||||||
|
-DWOLFSSL_HAVE_SP_ECC
|
||||||
|
-DWOLFSSL_SP_SMALL
|
||||||
-DHAVE_TLS_EXTENSIONS
|
-DHAVE_TLS_EXTENSIONS
|
||||||
-DHAVE_SUPPORTED_CURVES
|
-DHAVE_SUPPORTED_CURVES
|
||||||
-DHAVE_HKDF
|
-DHAVE_HKDF
|
||||||
@@ -63,6 +73,47 @@ board_build.flash_mode = dio
|
|||||||
board_build.flash_size = 16MB
|
board_build.flash_size = 16MB
|
||||||
board_build.partitions = partitions.csv
|
board_build.partitions = partitions.csv
|
||||||
|
|
||||||
|
; MEMFIX-PORT: custom_sdkconfig heap reclamation (~32-37 KB). Rebuilds the
|
||||||
|
; Arduino core libs on first build (slower once, cached after; needs the CMake
|
||||||
|
; pin in platformio.local.ini on macOS).
|
||||||
|
;
|
||||||
|
; If an interrupted rebuild fails with "multiple definition of 'app_main'"
|
||||||
|
; (stale generated scaffold), clean it up with:
|
||||||
|
; rm -rf .dummy CMakeLists.txt sdkconfig.default sdkconfig.defaults .pio/build/default
|
||||||
|
; Do NOT use `git clean -fdX` — it deletes platformio.local.ini.
|
||||||
|
custom_sdkconfig =
|
||||||
|
; Task stack right-sizing from measured high-water marks (heap block map +
|
||||||
|
; per-task stack audit, July 2026): esp_timer used ~0.8 KB of 8 KB across
|
||||||
|
; every capture; the FreeRTOS timer service used ~0.5 KB
|
||||||
|
; of 4 KB. Neither runs TLS or app code. ~7 KB back to the heap.
|
||||||
|
CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096
|
||||||
|
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2560
|
||||||
|
; Move the WiFi stack's non-critical hot paths out of IRAM into flash.
|
||||||
|
; On the C3, IRAM and DRAM share one SRAM pool, so the ~25-30 KB this
|
||||||
|
; frees lands directly in the heap — paid for with lower WiFi throughput
|
||||||
|
; during transfers (occasional sync/OTA use, not streaming: acceptable).
|
||||||
|
; IRAM cost is static, so the heap gain applies even with WiFi off.
|
||||||
|
CONFIG_ESP_WIFI_IRAM_OPT=n
|
||||||
|
CONFIG_ESP_WIFI_RX_IRAM_OPT=n
|
||||||
|
; Keep the Arduino wrappers for the removed cloud components (below) out of
|
||||||
|
; the core source list; all other bundled libraries default to enabled.
|
||||||
|
CONFIG_ARDUINO_SELECTIVE_COMPILATION=y
|
||||||
|
CONFIG_ARDUINO_SELECTIVE_RainMaker=n
|
||||||
|
CONFIG_ARDUINO_SELECTIVE_Insights=n
|
||||||
|
|
||||||
|
; Drop unused cloud components from the core rebuild. esp_insights/rainmaker
|
||||||
|
; require embedded server certs the lib builder can't generate
|
||||||
|
; ("https_server.crt.S not found"); this firmware uses none of them.
|
||||||
|
custom_component_remove =
|
||||||
|
espressif/esp_insights
|
||||||
|
espressif/esp_rainmaker
|
||||||
|
espressif/esp_diagnostics
|
||||||
|
espressif/esp_diag_data_store
|
||||||
|
espressif/esp_schedule
|
||||||
|
espressif/esp_rcp_update
|
||||||
|
espressif/esp_secure_cert_mgr
|
||||||
|
espressif/cbor
|
||||||
|
|
||||||
extra_scripts =
|
extra_scripts =
|
||||||
pre:scripts/patch_wolfssl.py
|
pre:scripts/patch_wolfssl.py
|
||||||
pre:scripts/build_html.py
|
pre:scripts/build_html.py
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ OVERRIDES = f"""
|
|||||||
#ifndef HAVE_FFDHE_2048
|
#ifndef HAVE_FFDHE_2048
|
||||||
#define HAVE_FFDHE_2048
|
#define HAVE_FFDHE_2048
|
||||||
#endif
|
#endif
|
||||||
|
/* MEMFIX-PORT: 8192 handles up to RSA-4096 keys (the public-CA maximum,
|
||||||
|
ISRG Root X1 included) with half the per-bignum heap of 16384: with
|
||||||
|
WOLFSSL_SMALL_STACK each fast-math temp is FP_MAX_BITS/8 * 2 bytes on the
|
||||||
|
heap, and TLS cert verification allocates dozens at once. */
|
||||||
#undef FP_MAX_BITS
|
#undef FP_MAX_BITS
|
||||||
#define FP_MAX_BITS 16384
|
#define FP_MAX_BITS 8192
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -58,14 +58,6 @@ class CrossPointSettings {
|
|||||||
HIDE_PROGRESS = 2,
|
HIDE_PROGRESS = 2,
|
||||||
STATUS_BAR_PROGRESS_BAR_COUNT
|
STATUS_BAR_PROGRESS_BAR_COUNT
|
||||||
};
|
};
|
||||||
// Values 0/1 keep the meaning of the old show/hide toggle (persisted under the
|
|
||||||
// legacy "statusBarChapterPageCount" key), so existing settings files load as-is.
|
|
||||||
enum STATUS_BAR_PAGE_COUNT {
|
|
||||||
HIDE_PAGE_COUNT = 0,
|
|
||||||
CHAPTER_PAGE_COUNT = 1,
|
|
||||||
BOOK_PAGE_COUNT = 2,
|
|
||||||
STATUS_BAR_PAGE_COUNT_COUNT
|
|
||||||
};
|
|
||||||
enum STATUS_BAR_PROGRESS_BAR_THICKNESS {
|
enum STATUS_BAR_PROGRESS_BAR_THICKNESS {
|
||||||
PROGRESS_BAR_THIN = 0,
|
PROGRESS_BAR_THIN = 0,
|
||||||
PROGRESS_BAR_NORMAL = 1,
|
PROGRESS_BAR_NORMAL = 1,
|
||||||
@@ -197,8 +189,7 @@ class CrossPointSettings {
|
|||||||
uint8_t sleepScreenCoverFilter = NO_FILTER;
|
uint8_t sleepScreenCoverFilter = NO_FILTER;
|
||||||
// Status bar settings (statusBar retained for migration only)
|
// Status bar settings (statusBar retained for migration only)
|
||||||
uint8_t statusBar = FULL;
|
uint8_t statusBar = FULL;
|
||||||
// STATUS_BAR_PAGE_COUNT; persisted under the legacy "statusBarChapterPageCount" key.
|
uint8_t statusBarChapterPageCount = 1;
|
||||||
uint8_t statusBarPageCount = CHAPTER_PAGE_COUNT;
|
|
||||||
uint8_t statusBarBookProgressPercentage = 1;
|
uint8_t statusBarBookProgressPercentage = 1;
|
||||||
uint8_t statusBarProgressBar = HIDE_PROGRESS;
|
uint8_t statusBarProgressBar = HIDE_PROGRESS;
|
||||||
uint8_t statusBarProgressBarThickness = PROGRESS_BAR_NORMAL;
|
uint8_t statusBarProgressBarThickness = PROGRESS_BAR_NORMAL;
|
||||||
|
|||||||
@@ -21,35 +21,35 @@
|
|||||||
void applyLegacyStatusBarSettings(CrossPointSettings& settings) {
|
void applyLegacyStatusBarSettings(CrossPointSettings& settings) {
|
||||||
switch (static_cast<CrossPointSettings::STATUS_BAR_MODE>(settings.statusBar)) {
|
switch (static_cast<CrossPointSettings::STATUS_BAR_MODE>(settings.statusBar)) {
|
||||||
case CrossPointSettings::NONE:
|
case CrossPointSettings::NONE:
|
||||||
settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT;
|
settings.statusBarChapterPageCount = 0;
|
||||||
settings.statusBarBookProgressPercentage = 0;
|
settings.statusBarBookProgressPercentage = 0;
|
||||||
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
|
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
|
||||||
settings.statusBarTitle = CrossPointSettings::HIDE_TITLE;
|
settings.statusBarTitle = CrossPointSettings::HIDE_TITLE;
|
||||||
settings.statusBarBattery = 0;
|
settings.statusBarBattery = 0;
|
||||||
break;
|
break;
|
||||||
case CrossPointSettings::NO_PROGRESS:
|
case CrossPointSettings::NO_PROGRESS:
|
||||||
settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT;
|
settings.statusBarChapterPageCount = 0;
|
||||||
settings.statusBarBookProgressPercentage = 0;
|
settings.statusBarBookProgressPercentage = 0;
|
||||||
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
|
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
|
||||||
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
|
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
|
||||||
settings.statusBarBattery = 1;
|
settings.statusBarBattery = 1;
|
||||||
break;
|
break;
|
||||||
case CrossPointSettings::BOOK_PROGRESS_BAR:
|
case CrossPointSettings::BOOK_PROGRESS_BAR:
|
||||||
settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT;
|
settings.statusBarChapterPageCount = 1;
|
||||||
settings.statusBarBookProgressPercentage = 0;
|
settings.statusBarBookProgressPercentage = 0;
|
||||||
settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS;
|
settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS;
|
||||||
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
|
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
|
||||||
settings.statusBarBattery = 1;
|
settings.statusBarBattery = 1;
|
||||||
break;
|
break;
|
||||||
case CrossPointSettings::ONLY_BOOK_PROGRESS_BAR:
|
case CrossPointSettings::ONLY_BOOK_PROGRESS_BAR:
|
||||||
settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT;
|
settings.statusBarChapterPageCount = 1;
|
||||||
settings.statusBarBookProgressPercentage = 0;
|
settings.statusBarBookProgressPercentage = 0;
|
||||||
settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS;
|
settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS;
|
||||||
settings.statusBarTitle = CrossPointSettings::HIDE_TITLE;
|
settings.statusBarTitle = CrossPointSettings::HIDE_TITLE;
|
||||||
settings.statusBarBattery = 0;
|
settings.statusBarBattery = 0;
|
||||||
break;
|
break;
|
||||||
case CrossPointSettings::CHAPTER_PROGRESS_BAR:
|
case CrossPointSettings::CHAPTER_PROGRESS_BAR:
|
||||||
settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT;
|
settings.statusBarChapterPageCount = 0;
|
||||||
settings.statusBarBookProgressPercentage = 1;
|
settings.statusBarBookProgressPercentage = 1;
|
||||||
settings.statusBarProgressBar = CrossPointSettings::CHAPTER_PROGRESS;
|
settings.statusBarProgressBar = CrossPointSettings::CHAPTER_PROGRESS;
|
||||||
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
|
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
|
||||||
@@ -57,7 +57,7 @@ void applyLegacyStatusBarSettings(CrossPointSettings& settings) {
|
|||||||
break;
|
break;
|
||||||
case CrossPointSettings::FULL:
|
case CrossPointSettings::FULL:
|
||||||
default:
|
default:
|
||||||
settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT;
|
settings.statusBarChapterPageCount = 1;
|
||||||
settings.statusBarBookProgressPercentage = 1;
|
settings.statusBarBookProgressPercentage = 1;
|
||||||
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
|
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
|
||||||
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
|
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
|
||||||
|
|||||||
+2
-4
@@ -233,10 +233,8 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
|
|||||||
},
|
},
|
||||||
"koSendMetadata", StrId::STR_KOREADER_SYNC),
|
"koSendMetadata", StrId::STR_KOREADER_SYNC),
|
||||||
// --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) ---
|
// --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) ---
|
||||||
// Key kept from the old show/hide toggle so existing settings files load unchanged.
|
SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount,
|
||||||
SettingInfo::Enum(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarPageCount,
|
"statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR),
|
||||||
{StrId::STR_HIDE, StrId::STR_CHAPTER, StrId::STR_BOOK}, "statusBarChapterPageCount",
|
|
||||||
StrId::STR_CUSTOMISE_STATUS_BAR),
|
|
||||||
SettingInfo::Toggle(StrId::STR_BOOK_PROGRESS_PERCENTAGE, &CrossPointSettings::statusBarBookProgressPercentage,
|
SettingInfo::Toggle(StrId::STR_BOOK_PROGRESS_PERCENTAGE, &CrossPointSettings::statusBarBookProgressPercentage,
|
||||||
"statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR),
|
"statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR),
|
||||||
SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar,
|
SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar,
|
||||||
|
|||||||
@@ -44,22 +44,6 @@ constexpr int PAGE_TURN_RATES[] = {1, 1, 3, 6, 12};
|
|||||||
constexpr size_t initialBookmarkCacheCapacity = 16;
|
constexpr size_t initialBookmarkCacheCapacity = 16;
|
||||||
constexpr float bookmarkProgressEpsilon = 0.0001f;
|
constexpr float bookmarkProgressEpsilon = 0.0001f;
|
||||||
|
|
||||||
// The render parameters the reader paginates with, as passed to loadSectionFile /
|
|
||||||
// createSectionFile / startBuild below. Used to validate other sections' cached
|
|
||||||
// page counts and to detect when a settings change invalidates the harvest.
|
|
||||||
Section::RenderParams readerRenderParams(const uint16_t viewportWidth, const uint16_t viewportHeight) {
|
|
||||||
return {SETTINGS.getReaderFontId(),
|
|
||||||
SETTINGS.getReaderLineCompression(),
|
|
||||||
static_cast<bool>(SETTINGS.extraParagraphSpacing),
|
|
||||||
SETTINGS.paragraphAlignment,
|
|
||||||
viewportWidth,
|
|
||||||
viewportHeight,
|
|
||||||
static_cast<bool>(SETTINGS.hyphenationEnabled),
|
|
||||||
static_cast<bool>(SETTINGS.embeddedStyle),
|
|
||||||
SETTINGS.imageRendering,
|
|
||||||
static_cast<bool>(SETTINGS.focusReadingEnabled)};
|
|
||||||
}
|
|
||||||
|
|
||||||
int clampPercent(int percent) {
|
int clampPercent(int percent) {
|
||||||
if (percent < 0) {
|
if (percent < 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -273,6 +257,18 @@ void EpubReaderActivity::openReaderMenu() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EpubReaderActivity::buildTickHeapGate() {
|
||||||
|
const size_t freeHeap = ESP.getFreeHeap();
|
||||||
|
const size_t maxBlock = ESP.getMaxAllocHeap();
|
||||||
|
// Below the floors: just wait. The tick is deferrable — page-turn transients
|
||||||
|
// free up between turns and the tick retries every loop pass. Track the
|
||||||
|
// paused state so skipLoopDelay() stops pinning the CPU at full speed while
|
||||||
|
// no build work is actually happening (the gate can stay closed for a long
|
||||||
|
// stretch if the retained build context itself holds the heap down).
|
||||||
|
buildHeapPaused = freeHeap < BACKGROUND_BUILD_MIN_FREE_HEAP || maxBlock < BACKGROUND_BUILD_MIN_MAX_ALLOC;
|
||||||
|
return !buildHeapPaused;
|
||||||
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::loop() {
|
void EpubReaderActivity::loop() {
|
||||||
if (!epub) {
|
if (!epub) {
|
||||||
// Should never happen
|
// Should never happen
|
||||||
@@ -280,6 +276,40 @@ void EpubReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Idle glyph prewarm for the likely next page (currentPage + 1). The scan
|
||||||
|
// pass draws nothing (FCM scan mode suppresses pixels), so the displayed
|
||||||
|
// framebuffer is untouched; endScanAndPrewarm loads only glyphs not already
|
||||||
|
// cached. Debounced past rapid page-flipping, one attempt per position, and
|
||||||
|
// deferred while a render/build owns the CPU or the heap is at the render
|
||||||
|
// floor. Cross-chapter prewarm is deliberately out of scope (next spine's
|
||||||
|
// section isn't loaded).
|
||||||
|
constexpr unsigned long IDLE_PREWARM_DEBOUNCE_MS = 400;
|
||||||
|
if (section && !section->isBuilding() && !RenderLock::peek() && renderer.hasFrameBuffer() &&
|
||||||
|
lastRenderCompleteMs != 0 && millis() - lastRenderCompleteMs > IDLE_PREWARM_DEBOUNCE_MS &&
|
||||||
|
ESP.getFreeHeap() > RENDER_MIN_FREE_HEAP && ESP.getMaxAllocHeap() > BACKGROUND_BUILD_MIN_MAX_ALLOC &&
|
||||||
|
(idlePrewarmSpine != currentSpineIndex || idlePrewarmPage != section->currentPage)) {
|
||||||
|
RenderLock lock; // the page table must not change under the scan
|
||||||
|
// Re-check under the lock: peek() and acquisition are not atomic, so the render
|
||||||
|
// task may have reset/replaced the section or moved the page in between.
|
||||||
|
if (section && !section->isBuilding() &&
|
||||||
|
(idlePrewarmSpine != currentSpineIndex || idlePrewarmPage != section->currentPage)) {
|
||||||
|
idlePrewarmSpine = currentSpineIndex;
|
||||||
|
idlePrewarmPage = section->currentPage;
|
||||||
|
const int nextPage = section->currentPage + 1;
|
||||||
|
if (nextPage < static_cast<int>(section->pageCount)) {
|
||||||
|
if (const auto p = section->loadPage(nextPage)) {
|
||||||
|
if (auto* fcm = renderer.getFontCacheManager()) {
|
||||||
|
const auto t0 = millis();
|
||||||
|
auto scope = fcm->createPrewarmScope();
|
||||||
|
p->render(renderer, SETTINGS.getReaderFontId(), 0, 0); // scan only, no pixels
|
||||||
|
scope.endScanAndPrewarm();
|
||||||
|
LOG_DBG("ERS", "Idle prewarm: page %d in %lums", nextPage, millis() - t0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lazily resume a partial's extension build once the reader nears its watermark. Far from
|
// Lazily resume a partial's extension build once the reader nears its watermark. Far from
|
||||||
// it the rebuild is all cost (whole-chapter re-layout from page 0) and no benefit this
|
// it the rebuild is all cost (whole-chapter re-layout from page 0) and no benefit this
|
||||||
// session, so reopening a partial deliberately does NOT start it (see the deferral in
|
// session, so reopening a partial deliberately does NOT start it (see the deferral in
|
||||||
@@ -315,14 +345,17 @@ void EpubReaderActivity::loop() {
|
|||||||
// "far enough ahead" and stall the build at 0 pages -- then the first turn past the
|
// "far enough ahead" and stall the build at 0 pages -- then the first turn past the
|
||||||
// watermark re-parses the whole chapter synchronously. Keep ticking until it finalizes.
|
// watermark re-parses the whole chapter synchronously. Keep ticking until it finalizes.
|
||||||
if (section && section->isBuilding() && !RenderLock::peek() &&
|
if (section && section->isBuilding() && !RenderLock::peek() &&
|
||||||
(section->isPartial() || static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD)) {
|
(section->isPartial() || static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD) &&
|
||||||
|
buildTickHeapGate()) {
|
||||||
RenderLock lock;
|
RenderLock lock;
|
||||||
// Re-check under the lock: render() (which also holds the RenderLock) may have finalized the
|
// Re-check under the lock: render() (which also holds the RenderLock) may have finalized the
|
||||||
// build between the outer isBuilding() check and acquiring the lock here, in which case
|
// build between the outer isBuilding() check and acquiring the lock here, in which case
|
||||||
// buildSomeMore() would fail and wrongly reset the section. cppcheck can't see the cross-task
|
// buildSomeMore() would fail and wrongly reset the section. The heap gate must be re-read
|
||||||
// mutation, so it flags this as always true.
|
// too: a render that won the lock race can expand retained glyph buffers, invalidating the
|
||||||
|
// pre-lock heap reading. cppcheck can't see the cross-task mutation, so it flags this as
|
||||||
|
// always true.
|
||||||
// cppcheck-suppress knownConditionTrueFalse
|
// cppcheck-suppress knownConditionTrueFalse
|
||||||
if (section->isBuilding()) {
|
if (section->isBuilding() && buildTickHeapGate()) {
|
||||||
if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) {
|
if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) {
|
||||||
LOG_ERR("ERS", "Background section build failed");
|
LOG_ERR("ERS", "Background section build failed");
|
||||||
section.reset();
|
section.reset();
|
||||||
@@ -335,25 +368,6 @@ void EpubReaderActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whole-book page counter: harvest one other section's cached page count per tick
|
|
||||||
// (a header-only peek), so the total converges to exact without visiting every
|
|
||||||
// chapter. Idle-priority — skipped whenever a render is pending or a build is
|
|
||||||
// running. No re-render is requested; the counter refreshes on the next page turn.
|
|
||||||
if (bookPages && bookPagesSweepIndex < epub->getSpineItemsCount() && !RenderLock::peek() &&
|
|
||||||
!(section && section->isBuilding())) {
|
|
||||||
RenderLock lock;
|
|
||||||
// Re-check under the lock: render() may have just reset/reallocated the table.
|
|
||||||
if (bookPages && bookPagesSweepIndex < epub->getSpineItemsCount()) {
|
|
||||||
const int index = bookPagesSweepIndex++;
|
|
||||||
if (bookPages[index].pages < 0) {
|
|
||||||
const Section peekSection(epub, index, renderer);
|
|
||||||
if (const auto count = peekSection.getCachedPageCount(&bookPagesParams)) {
|
|
||||||
bookPages[index].pages = *count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// End-of-Book screen reached (currentSpineIndex == spine count) means the book is
|
// End-of-Book screen reached (currentSpineIndex == spine count) means the book is
|
||||||
// finished. Two independent finished-book features key off this same condition.
|
// finished. Two independent finished-book features key off this same condition.
|
||||||
const bool atEndOfBook = currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount();
|
const bool atEndOfBook = currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount();
|
||||||
@@ -1015,8 +1029,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
|||||||
buildViewportWidth = viewportWidth;
|
buildViewportWidth = viewportWidth;
|
||||||
buildViewportHeight = viewportHeight;
|
buildViewportHeight = viewportHeight;
|
||||||
|
|
||||||
ensureBookPages(viewportWidth, viewportHeight);
|
|
||||||
|
|
||||||
if (!section) {
|
if (!section) {
|
||||||
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
|
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
|
||||||
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
|
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
|
||||||
@@ -1259,8 +1271,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
|||||||
// a plain resume / unchanged pagination). If still building, this defers to loop() on completion.
|
// a plain resume / unchanged pagination). If still building, this defers to loop() on completion.
|
||||||
applyDeferredReposition();
|
applyDeferredReposition();
|
||||||
|
|
||||||
recordCurrentSectionPages();
|
|
||||||
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
if (section->pageCount == 0) {
|
if (section->pageCount == 0) {
|
||||||
@@ -1321,6 +1331,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
|||||||
const auto start = millis();
|
const auto start = millis();
|
||||||
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
||||||
|
lastRenderCompleteMs = millis();
|
||||||
}
|
}
|
||||||
// Only persist when the position actually changed. render() also runs on menu,
|
// Only persist when the position actually changed. render() also runs on menu,
|
||||||
// bookmark and screenshot re-renders, and writeAtomic is several FAT ops for 6 bytes.
|
// bookmark and screenshot re-renders, and writeAtomic is several FAT ops for 6 bytes.
|
||||||
@@ -1370,59 +1381,6 @@ bool EpubReaderActivity::applyDeferredReposition() {
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::ensureBookPages(const uint16_t viewportWidth, const uint16_t viewportHeight) {
|
|
||||||
if (SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::BOOK_PAGE_COUNT) {
|
|
||||||
bookPages.reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Section::RenderParams params = readerRenderParams(viewportWidth, viewportHeight);
|
|
||||||
if (bookPages && params == bookPagesParams) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// First render, or a render-params change (font/orientation/...): the harvested
|
|
||||||
// counts are for the old pagination, so start over. The section caches themselves
|
|
||||||
// are the persistence; there is nothing else to invalidate.
|
|
||||||
bookPagesParams = params;
|
|
||||||
bookPagesSweepIndex = 0;
|
|
||||||
bookPages.reset();
|
|
||||||
const int sectionCount = epub->getSpineItemsCount();
|
|
||||||
if (sectionCount <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 8 bytes per spine item, held for the reading session; freed with the activity
|
|
||||||
// or when the feature is switched off. On OOM stays null (chapter-local counts).
|
|
||||||
bookPages = makeUniqueNoThrow<BookPageEntry[]>(sectionCount);
|
|
||||||
if (!bookPages) {
|
|
||||||
LOG_ERR("ERS", "OOM: book page table (%d sections)", sectionCount);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
size_t prev = 0;
|
|
||||||
for (int i = 0; i < sectionCount; ++i) {
|
|
||||||
const size_t cum = epub->getCumulativeSpineItemSize(i);
|
|
||||||
bookPages[i].bytes = static_cast<uint32_t>(cum >= prev ? cum - prev : 0);
|
|
||||||
prev = cum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderActivity::recordCurrentSectionPages() {
|
|
||||||
// Only a finalized section's pageCount is the chapter total; a building or
|
|
||||||
// partial section's is just its current watermark.
|
|
||||||
if (!bookPages || !section || section->isBuilding() || section->isPartial()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentSpineIndex >= 0 && currentSpineIndex < epub->getSpineItemsCount()) {
|
|
||||||
bookPages[currentSpineIndex].pages = section->pageCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<BookPagePosition> EpubReaderActivity::bookPagePosition() const {
|
|
||||||
if (!bookPages || !section) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return computeBookPagePosition(bookPages.get(), epub->getSpineItemsCount(), currentSpineIndex, section->currentPage,
|
|
||||||
section->estimatedTotalPages());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
|
bool EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
|
||||||
return EpubReaderUtils::saveProgress(*epub, spineIndex, currentPage, pageCount);
|
return EpubReaderUtils::saveProgress(*epub, spineIndex, currentPage, pageCount);
|
||||||
}
|
}
|
||||||
@@ -1443,6 +1401,13 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
const bool pageHasImagesNeedingDecode = pageHasImages && page->hasImagesNeedingDecode();
|
const bool pageHasImagesNeedingDecode = pageHasImages && page->hasImagesNeedingDecode();
|
||||||
const bool needsTextGrayscale = SETTINGS.textAntiAliasing;
|
const bool needsTextGrayscale = SETTINGS.textAntiAliasing;
|
||||||
const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages;
|
const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages;
|
||||||
|
const bool tiledGrayscale = needsAnyGrayscale && renderer.supportsStripGrayscale();
|
||||||
|
// Whole-plane buffering only pays when the BW refresh genuinely runs async
|
||||||
|
// underneath it; on blocking panels (X3) it would just spend ~50 KB for the
|
||||||
|
// identical serial timing. Image pages take the blocking double-FAST path
|
||||||
|
// below (no async refresh is ever started), so they'd spend the buffers with
|
||||||
|
// nothing in flight to overlap.
|
||||||
|
const bool overlapRefresh = tiledGrayscale && renderer.supportsAsyncRefresh() && !pageHasImages;
|
||||||
auto renderGrayscalePass = [&]() {
|
auto renderGrayscalePass = [&]() {
|
||||||
if (needsTextGrayscale) {
|
if (needsTextGrayscale) {
|
||||||
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
|
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
|
||||||
@@ -1488,28 +1453,112 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
// regardless of residue.
|
// regardless of residue.
|
||||||
pagesUntilFullRefresh = 1;
|
pagesUntilFullRefresh = 1;
|
||||||
} else {
|
} else {
|
||||||
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
// Async form: start the waveform and return so the grayscale plane rendering
|
||||||
|
// below overlaps the panel's refresh time instead of following it.
|
||||||
|
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh, overlapRefresh);
|
||||||
}
|
}
|
||||||
const auto tDisplay = millis();
|
const auto tDisplay = millis();
|
||||||
|
|
||||||
// Tiled grayscale: render each plane band-by-band into a small scratch and
|
// Tiled grayscale: render each plane band-by-band, leaving the BW
|
||||||
// stream straight to the controller, leaving the BW framebuffer intact so no
|
// framebuffer intact so no full-frame storeBwBuffer is needed; controller
|
||||||
// full-frame storeBwBuffer is needed; controller RAM is re-synced from the
|
// RAM is re-synced from the live framebuffer afterward. The page is
|
||||||
// live framebuffer afterward. The page is re-rendered ceil(H/STRIP_ROWS) times
|
// re-rendered ceil(H/STRIP_ROWS) times per plane, but renderCharImpl culls
|
||||||
// per plane, but renderCharImpl culls out-of-band glyphs before decode so the
|
// out-of-band glyphs before decode so the cost stays close to one render.
|
||||||
// cost stays close to one render. Both text (drawPixel) and images
|
// Both text (drawPixel) and images (DirectPixelWriter) honor the active
|
||||||
// (DirectPixelWriter) honor the active strip target.
|
// strip target. When the BW refresh above went out async, the plane
|
||||||
if (needsAnyGrayscale && renderer.supportsStripGrayscale()) {
|
// rendering below overlaps the panel's refresh time; only the controller
|
||||||
|
// RAM writes wait for BUSY.
|
||||||
|
if (tiledGrayscale) {
|
||||||
constexpr int STRIP_ROWS = 80;
|
constexpr int STRIP_ROWS = 80;
|
||||||
const int gh = renderer.getDisplayHeight();
|
const int gh = renderer.getDisplayHeight();
|
||||||
const int gwBytes = renderer.getDisplayWidthBytes();
|
const int gwBytes = renderer.getDisplayWidthBytes();
|
||||||
|
const size_t planeBytes = static_cast<size_t>(gwBytes) * gh;
|
||||||
|
|
||||||
|
// Render one plane band-by-band into a whole-plane buffer without touching
|
||||||
|
// the controller, so it can run while the refresh is still in flight.
|
||||||
|
auto renderPlaneToBuffer = [&](const bool lsbPlane, uint8_t* buf) {
|
||||||
|
renderer.setRenderMode(lsbPlane ? GfxRenderer::GRAYSCALE_LSB : 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(buf + static_cast<size_t>(y) * gwBytes, y, rows);
|
||||||
|
renderer.clearScreen(0x00);
|
||||||
|
renderGrayscalePass();
|
||||||
|
renderer.endStripTarget();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tiered on heap pressure: two plane buffers hide both plane renders
|
||||||
|
// inside the refresh wait; one hides the LSB render (its buffer is reused
|
||||||
|
// for MSB after streaming); none falls back to the strip-scratch flow with
|
||||||
|
// no overlap. Each buffer is only attempted when it leaves ~60 KB free so
|
||||||
|
// the pass never starves concurrent allocations: the next page re-render
|
||||||
|
// allocates through throwing std::string paths that abort() on OOM under
|
||||||
|
// -fno-exceptions, so a plane buffer that "fits" but eats the render
|
||||||
|
// headroom is worse than the strip fallback. Blocking panels skip the
|
||||||
|
// buffers entirely (nothing to overlap).
|
||||||
|
constexpr size_t PLANE_BUF_HEADROOM = 60000;
|
||||||
|
// Free-heap alone ignores fragmentation: taking the largest block for a
|
||||||
|
// plane can leave only slivers behind even when total headroom looks fine.
|
||||||
|
// Require the block to fit the plane with 16 KB contiguous to spare, which
|
||||||
|
// also keeps the advance-table batch scratch viable mid-render (same
|
||||||
|
// rationale as BACKGROUND_BUILD_MIN_MAX_ALLOC).
|
||||||
|
constexpr size_t PLANE_BUF_MAX_ALLOC_RESERVE = 16 * 1024;
|
||||||
|
const auto planeBufFits = [planeBytes] {
|
||||||
|
return ESP.getFreeHeap() >= planeBytes + PLANE_BUF_HEADROOM &&
|
||||||
|
ESP.getMaxAllocHeap() >= planeBytes + PLANE_BUF_MAX_ALLOC_RESERVE;
|
||||||
|
};
|
||||||
|
auto lsbPlaneBuf = (overlapRefresh && planeBufFits()) ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : nullptr;
|
||||||
|
auto msbPlaneBuf = (lsbPlaneBuf && planeBufFits()) ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : nullptr;
|
||||||
|
|
||||||
|
if (lsbPlaneBuf) {
|
||||||
|
renderPlaneToBuffer(true, lsbPlaneBuf.get());
|
||||||
|
if (msbPlaneBuf) renderPlaneToBuffer(false, msbPlaneBuf.get());
|
||||||
|
const auto tGrayRender = millis();
|
||||||
|
|
||||||
|
renderer.waitRefreshComplete();
|
||||||
|
const auto tWait = millis();
|
||||||
|
|
||||||
|
renderer.writeGrayscalePlaneStrip(true, lsbPlaneBuf.get(), 0, gh);
|
||||||
|
if (msbPlaneBuf) {
|
||||||
|
renderer.writeGrayscalePlaneStrip(false, msbPlaneBuf.get(), 0, gh);
|
||||||
|
} else {
|
||||||
|
renderPlaneToBuffer(false, lsbPlaneBuf.get());
|
||||||
|
renderer.writeGrayscalePlaneStrip(false, lsbPlaneBuf.get(), 0, gh);
|
||||||
|
}
|
||||||
|
const auto tGrayWrite = 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 tEnd = millis();
|
||||||
|
|
||||||
|
LOG_DBG("ERS",
|
||||||
|
"Page render (tiled async): prewarm=%lums bw_render=%lums display=%lums gray_render=%lums "
|
||||||
|
"wait=%lums gray_write=%lums gray_display=%lums cleanup=%lums total=%lums (planes buffered: %d)",
|
||||||
|
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayRender - tDisplay, tWait - tGrayRender,
|
||||||
|
tGrayWrite - tWait, tGrayDisplay - tGrayWrite, tEnd - tGrayDisplay, tEnd - t0, msbPlaneBuf ? 2 : 1);
|
||||||
|
} else {
|
||||||
|
// Per-strip scratch tier: blocking panels (X3) and the OOM fallback.
|
||||||
|
// The strip writes below need the panel idle, so wait out any pending
|
||||||
|
// async refresh first (no-op on blocking panels).
|
||||||
auto scratch = makeUniqueNoThrow<uint8_t[]>(static_cast<size_t>(gwBytes) * STRIP_ROWS);
|
auto scratch = makeUniqueNoThrow<uint8_t[]>(static_cast<size_t>(gwBytes) * STRIP_ROWS);
|
||||||
|
renderer.waitRefreshComplete();
|
||||||
if (!scratch) {
|
if (!scratch) {
|
||||||
LOG_ERR("ERS", "OOM: grayscale strip scratch (%d bytes); skipping AA this page", gwBytes * STRIP_ROWS);
|
LOG_ERR("ERS", "OOM: grayscale strip scratch (%d bytes); skipping AA this page", gwBytes * STRIP_ROWS);
|
||||||
|
if (overlapRefresh) {
|
||||||
|
// The BW refresh ran the shadow-free async path, so controller RAM's
|
||||||
|
// differential baseline was never rebuilt. Even with AA skipped it must
|
||||||
|
// be re-synced from the intact BW framebuffer, or the next differential
|
||||||
|
// update diffs against stale contents.
|
||||||
|
renderer.cleanupGrayscaleWithFrameBuffer();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Bands may be streamed in any order: X4 windows each via setRamArea, X3
|
// Bands may be streamed in any order: X4 windows each via setRamArea,
|
||||||
// via PTL.
|
// X3 via PTL.
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||||
for (int y = 0; y < gh; y += STRIP_ROWS) {
|
for (int y = 0; y < gh; y += STRIP_ROWS) {
|
||||||
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
|
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
|
||||||
@@ -1549,6 +1598,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayLsb - tDisplay, tGrayMsb - tGrayLsb,
|
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayLsb - tDisplay, tGrayMsb - tGrayLsb,
|
||||||
tGrayDisplay - tGrayMsb, tCleanup - tGrayDisplay, tEnd - t0);
|
tGrayDisplay - tGrayMsb, tCleanup - tGrayDisplay, tEnd - t0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback path for a controller without strip support. grayscale rendering
|
// Fallback path for a controller without strip support. grayscale rendering
|
||||||
// TODO: Only do this if font supports it
|
// TODO: Only do this if font supports it
|
||||||
@@ -1604,21 +1654,10 @@ void EpubReaderActivity::renderStatusBar() const {
|
|||||||
// Calculate progress in book. Use the estimated total while a giant spine is still building so
|
// Calculate progress in book. Use the estimated total while a giant spine is still building so
|
||||||
// "page X of Y" and the progress bar don't read off the small build watermark.
|
// "page X of Y" and the progress bar don't read off the small build watermark.
|
||||||
const int currentPage = section->currentPage + 1;
|
const int currentPage = section->currentPage + 1;
|
||||||
const int pageCount = section->estimatedTotalPages();
|
const float pageCount = section->estimatedTotalPages();
|
||||||
const float sectionChapterProg = (pageCount > 0) ? (static_cast<float>(currentPage) / pageCount) : 0;
|
const float sectionChapterProg = (pageCount > 0) ? (static_cast<float>(currentPage) / pageCount) : 0;
|
||||||
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
|
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
|
||||||
|
|
||||||
// Page counter values: whole-book numbers when Page Count is set to Book (and the
|
|
||||||
// table is alive), otherwise chapter-local. The progress bar stays on bookProgress.
|
|
||||||
int counterPage = currentPage;
|
|
||||||
int counterTotal = pageCount;
|
|
||||||
bool counterIsEstimate = section->isBuilding();
|
|
||||||
if (const auto book = bookPagePosition()) {
|
|
||||||
counterPage = book->currentPage;
|
|
||||||
counterTotal = book->totalPages;
|
|
||||||
counterIsEstimate = book->isEstimate;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string title;
|
std::string title;
|
||||||
|
|
||||||
int textYOffset = 0;
|
int textYOffset = 0;
|
||||||
@@ -1646,8 +1685,8 @@ void EpubReaderActivity::renderStatusBar() const {
|
|||||||
title = epub->getTitle();
|
title = epub->getTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
GUI.drawStatusBar(renderer, bookProgress, counterPage, counterTotal, title, 0, textYOffset, true,
|
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked,
|
||||||
currentPageBookmarked, counterIsEstimate);
|
section->isBuilding());
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
|
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <Epub/BookPages.h>
|
|
||||||
#include <Epub/FootnoteEntry.h>
|
#include <Epub/FootnoteEntry.h>
|
||||||
#include <Epub/Section.h>
|
#include <Epub/Section.h>
|
||||||
|
|
||||||
@@ -24,17 +23,6 @@ class EpubReaderActivity final : public Activity {
|
|||||||
int pagesUntilFullRefresh = 0;
|
int pagesUntilFullRefresh = 0;
|
||||||
int cachedSpineIndex = 0;
|
int cachedSpineIndex = 0;
|
||||||
int cachedChapterTotalPageCount = 0;
|
int cachedChapterTotalPageCount = 0;
|
||||||
// Whole-book page accounting ("page X of Y" across the whole book), active only
|
|
||||||
// when the status bar Page Count is set to Book. Exact counts are harvested from
|
|
||||||
// finalized section caches — nothing extra is persisted (see BookPages.h). Null
|
|
||||||
// while inactive or on OOM; every book-page path degrades to chapter-local counts.
|
|
||||||
// Shared between the render task and loop(): only touch under the RenderLock.
|
|
||||||
std::unique_ptr<BookPageEntry[]> bookPages;
|
|
||||||
// Render params the harvested counts are valid for; a change resets the harvest.
|
|
||||||
Section::RenderParams bookPagesParams;
|
|
||||||
// Next spine index for loop()'s background sweep that peeks other sections'
|
|
||||||
// cached counts (one per tick); >= spine count once the sweep is done.
|
|
||||||
int bookPagesSweepIndex = 0;
|
|
||||||
unsigned long lastPageTurnTime = 0UL;
|
unsigned long lastPageTurnTime = 0UL;
|
||||||
unsigned long pageTurnDuration = 0UL;
|
unsigned long pageTurnDuration = 0UL;
|
||||||
// Signals that the next render should reposition within the newly loaded section
|
// Signals that the next render should reposition within the newly loaded section
|
||||||
@@ -53,6 +41,13 @@ class EpubReaderActivity final : public Activity {
|
|||||||
bool showBookmarkMessage = false;
|
bool showBookmarkMessage = false;
|
||||||
bool ignoreNextConfirmRelease = false;
|
bool ignoreNextConfirmRelease = false;
|
||||||
bool currentPageBookmarked = false;
|
bool currentPageBookmarked = false;
|
||||||
|
// Idle-time glyph prewarm: after a page settles, scan the LIKELY next page
|
||||||
|
// (scan mode draws nothing) and load its missing glyphs from SD during idle,
|
||||||
|
// so the next turn's in-render prewarm is a cache hit instead of ~100 ms of
|
||||||
|
// SD reads on the page-turn critical path. One attempt per position.
|
||||||
|
int idlePrewarmSpine = -1;
|
||||||
|
int idlePrewarmPage = -1;
|
||||||
|
unsigned long lastRenderCompleteMs = 0;
|
||||||
bool bookmarkRemoved = false; // true when last toggle removed (controls popup text)
|
bool bookmarkRemoved = false; // true when last toggle removed (controls popup text)
|
||||||
std::vector<BookmarkEntry> cachedBookmarks;
|
std::vector<BookmarkEntry> cachedBookmarks;
|
||||||
// Tracks whether this book is currently removed from Recent Books by the
|
// Tracks whether this book is currently removed from Recent Books by the
|
||||||
@@ -98,6 +93,33 @@ class EpubReaderActivity final : public Activity {
|
|||||||
// background build chunk never noticeably delays input or a pending render.
|
// background build chunk never noticeably delays input or a pending render.
|
||||||
static constexpr int BUILD_PAGES_PER_CHUNK = 8;
|
static constexpr int BUILD_PAGES_PER_CHUNK = 8;
|
||||||
static constexpr int BACKGROUND_BUILD_PAGES_PER_TICK = 2;
|
static constexpr int BACKGROUND_BUILD_PAGES_PER_TICK = 2;
|
||||||
|
|
||||||
|
// MEMFIX-PORT: background-build heap floor; portable
|
||||||
|
// Skip background build ticks below this free-heap floor. The parse path grows
|
||||||
|
// word vectors of heap strings — throwing allocations that abort() on OOM under
|
||||||
|
// -fno-exceptions (field crash: bad_alloc in ParsedText::addWord during a
|
||||||
|
// background tick under heap pressure). The tick is deferrable work:
|
||||||
|
// page-turn transients free up between turns and the build resumes; the render
|
||||||
|
// path still builds the page it actually needs regardless of this floor.
|
||||||
|
static constexpr size_t BACKGROUND_BUILD_MIN_FREE_HEAP = 32 * 1024;
|
||||||
|
// Fragmentation floor for the same gate: a tick passed the free-heap floor at
|
||||||
|
// 34.7 KB free but the largest block was ~11 KB, and a parse allocation inside the
|
||||||
|
// tick aborted anyway. Free heap says how much memory exists; maxAlloc says whether
|
||||||
|
// any single allocation can actually have it. 16 KB also keeps the advance-table
|
||||||
|
// batch path (16 KB scratch) viable during builds.
|
||||||
|
static constexpr size_t BACKGROUND_BUILD_MIN_MAX_ALLOC = 16 * 1024;
|
||||||
|
// Gate for a background build tick: true when the heap can take parse allocations.
|
||||||
|
// Updates buildHeapPaused as a side effect.
|
||||||
|
bool buildTickHeapGate();
|
||||||
|
// True while the background build is gated on the heap floors. Lets skipLoopDelay()
|
||||||
|
// return the loop to normal delay/power-saving during the pause: isBuilding() stays
|
||||||
|
// true the whole time, and without this the loop would spin at full CPU speed doing
|
||||||
|
// no build work — indefinitely, if the build context itself keeps the heap low.
|
||||||
|
bool buildHeapPaused = false;
|
||||||
|
// Heap floor for optional render-adjacent work (idle prewarm). Page
|
||||||
|
// deserialization (TextBlock word vectors/strings) and glyph caching allocate
|
||||||
|
// through throwing paths that abort() on OOM; skip deferrable work below it.
|
||||||
|
static constexpr size_t RENDER_MIN_FREE_HEAP = 24 * 1024;
|
||||||
// How many pages to keep laid out ahead of the reader for a still-building section. A page
|
// How many pages to keep laid out ahead of the reader for a still-building section. A page
|
||||||
// turn is ~1s on e-ink and a page builds in ~30ms, so the reader can't out-click the builder
|
// turn is ~1s on e-ink and a page builds in ~30ms, so the reader can't out-click the builder
|
||||||
// -- a tiny buffer is enough. The background build stops once the watermark is this far
|
// -- a tiny buffer is enough. The background build stops once the watermark is this far
|
||||||
@@ -125,14 +147,6 @@ class EpubReaderActivity final : public Activity {
|
|||||||
// (used after a settings change re-paginates a chapter). Returns true if currentPage moved.
|
// (used after a settings change re-paginates a chapter). Returns true if currentPage moved.
|
||||||
// No-op while the section is still building or when the pagination is unchanged (plain resume).
|
// No-op while the section is still building or when the pagination is unchanged (plain resume).
|
||||||
bool applyDeferredReposition();
|
bool applyDeferredReposition();
|
||||||
// (Re)allocate and reset bookPages when the feature turns on or the render params
|
|
||||||
// change. Called from render() (under the RenderLock) where the viewport is known.
|
|
||||||
void ensureBookPages(uint16_t viewportWidth, uint16_t viewportHeight);
|
|
||||||
// Store the current section's exact page count once its pagination is final
|
|
||||||
// (no-op while building or partial). Caller must hold the RenderLock.
|
|
||||||
void recordCurrentSectionPages();
|
|
||||||
// Book-global position for the current page, or nullopt while inactive.
|
|
||||||
std::optional<BookPagePosition> bookPagePosition() const;
|
|
||||||
bool saveProgress(int spineIndex, int currentPage, int pageCount);
|
bool saveProgress(int spineIndex, int currentPage, int pageCount);
|
||||||
// Jump to a percentage of the book (0-100), mapping it to spine and page.
|
// Jump to a percentage of the book (0-100), mapping it to spine and page.
|
||||||
void jumpToPercent(int percent);
|
void jumpToPercent(int percent);
|
||||||
@@ -163,8 +177,10 @@ class EpubReaderActivity final : public Activity {
|
|||||||
// Full CPU speed + fast loop ticks while a section build runs: at the low-power
|
// Full CPU speed + fast loop ticks while a section build runs: at the low-power
|
||||||
// frequency a giant chapter's background rebuild stretches from ~40s to many
|
// frequency a giant chapter's background rebuild stretches from ~40s to many
|
||||||
// minutes, so the reader exits before it can finalize and the next open restarts
|
// minutes, so the reader exits before it can finalize and the next open restarts
|
||||||
// it from page 0. Reverts to normal power behavior the moment the build finishes.
|
// it from page 0. Reverts to normal power behavior the moment the build finishes,
|
||||||
bool skipLoopDelay() override { return section && section->isBuilding(); }
|
// and while the build is heap-paused (no work is happening, so spinning at full
|
||||||
|
// speed would only burn battery; the paused gate still retries every loop pass).
|
||||||
|
bool skipLoopDelay() override { return section && section->isBuilding() && !buildHeapPaused; }
|
||||||
bool isReaderActivity() const override { return true; }
|
bool isReaderActivity() const override { return true; }
|
||||||
ScreenshotInfo getScreenshotInfo() const override;
|
ScreenshotInfo getScreenshotInfo() const override;
|
||||||
CrossPointPosition getCurrentPosition() const;
|
CrossPointPosition getCurrentPosition() const;
|
||||||
|
|||||||
@@ -59,12 +59,21 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
|
|||||||
return {prev, next, tiltPrev || tiltNext};
|
return {prev, next, tiltPrev || tiltNext};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) {
|
// One helper, blocking or deferred: the async form starts the refresh and
|
||||||
|
// returns so the caller can overlap CPU work with the panel's refresh time.
|
||||||
|
// Async callers must not touch the framebuffer until
|
||||||
|
// renderer.waitRefreshComplete() and must rebuild the differential baseline
|
||||||
|
// before the next page turn (the tiled grayscale cleanup does).
|
||||||
|
inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh, bool async = false) {
|
||||||
|
const auto mode = (pagesUntilFullRefresh <= 1) ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH;
|
||||||
|
if (async) {
|
||||||
|
renderer.displayBufferAsync(mode);
|
||||||
|
} else {
|
||||||
|
renderer.displayBuffer(mode);
|
||||||
|
}
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
||||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||||
} else {
|
} else {
|
||||||
renderer.displayBuffer();
|
|
||||||
pagesUntilFullRefresh--;
|
pagesUntilFullRefresh--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,10 +64,6 @@ std::string formatUtcOffset(uint8_t biasedQ) {
|
|||||||
snprintf(buf, sizeof(buf), "UTC%c%d:%02d", neg ? '-' : '+', hours, mins);
|
snprintf(buf, sizeof(buf), "UTC%c%d:%02d", neg ? '-' : '+', hours, mins);
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
// Order follows STATUS_BAR_PAGE_COUNT (hide=0, chapter=1, book=2).
|
|
||||||
constexpr int PAGE_COUNT_ITEMS = 3;
|
|
||||||
const StrId pageCountNames[PAGE_COUNT_ITEMS] = {StrId::STR_HIDE, StrId::STR_CHAPTER, StrId::STR_BOOK};
|
|
||||||
|
|
||||||
constexpr int PROGRESS_BAR_ITEMS = 3;
|
constexpr int PROGRESS_BAR_ITEMS = 3;
|
||||||
const StrId progressBarNames[PROGRESS_BAR_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE};
|
const StrId progressBarNames[PROGRESS_BAR_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE};
|
||||||
|
|
||||||
@@ -94,11 +90,7 @@ void StatusBarSettingsActivity::onEnter() {
|
|||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
visibleItemCount = halClock.isAvailable() ? FULL_MENU_ITEMS : BASE_MENU_ITEMS;
|
visibleItemCount = halClock.isAvailable() ? FULL_MENU_ITEMS : BASE_MENU_ITEMS;
|
||||||
|
|
||||||
// Clamp enum-valued settings in case of corrupt/migrated data
|
// Clamp statusBarProgressBar and statusBarTitle in case of corrupt/migrated data
|
||||||
if (SETTINGS.statusBarPageCount >= PAGE_COUNT_ITEMS) {
|
|
||||||
SETTINGS.statusBarPageCount = CrossPointSettings::STATUS_BAR_PAGE_COUNT::CHAPTER_PAGE_COUNT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SETTINGS.statusBarProgressBar >= PROGRESS_BAR_ITEMS) {
|
if (SETTINGS.statusBarProgressBar >= PROGRESS_BAR_ITEMS) {
|
||||||
SETTINGS.statusBarProgressBar = CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS;
|
SETTINGS.statusBarProgressBar = CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS;
|
||||||
}
|
}
|
||||||
@@ -171,12 +163,8 @@ void StatusBarSettingsActivity::loop() {
|
|||||||
void StatusBarSettingsActivity::handleSelection() {
|
void StatusBarSettingsActivity::handleSelection() {
|
||||||
switch (selectedIndex) {
|
switch (selectedIndex) {
|
||||||
case ITEM_CHAPTER_PAGE_COUNT:
|
case ITEM_CHAPTER_PAGE_COUNT:
|
||||||
optionPopup.show(StrId::STR_CHAPTER_PAGE_COUNT, pageCountNames, PAGE_COUNT_ITEMS, SETTINGS.statusBarPageCount,
|
SETTINGS.statusBarChapterPageCount = (SETTINGS.statusBarChapterPageCount + 1) % 2;
|
||||||
[this](int idx) {
|
break;
|
||||||
SETTINGS.statusBarPageCount = idx;
|
|
||||||
SETTINGS.saveToFile();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
case ITEM_BOOK_PROGRESS_PERCENTAGE:
|
case ITEM_BOOK_PROGRESS_PERCENTAGE:
|
||||||
SETTINGS.statusBarBookProgressPercentage = (SETTINGS.statusBarBookProgressPercentage + 1) % 2;
|
SETTINGS.statusBarBookProgressPercentage = (SETTINGS.statusBarBookProgressPercentage + 1) % 2;
|
||||||
break;
|
break;
|
||||||
@@ -248,7 +236,7 @@ void StatusBarSettingsActivity::render(RenderLock&&) {
|
|||||||
[](int index) -> std::string {
|
[](int index) -> std::string {
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case ITEM_CHAPTER_PAGE_COUNT:
|
case ITEM_CHAPTER_PAGE_COUNT:
|
||||||
return I18N.get(pageCountNames[SETTINGS.statusBarPageCount]);
|
return SETTINGS.statusBarChapterPageCount ? tr(STR_SHOW) : tr(STR_HIDE);
|
||||||
case ITEM_BOOK_PROGRESS_PERCENTAGE:
|
case ITEM_BOOK_PROGRESS_PERCENTAGE:
|
||||||
return SETTINGS.statusBarBookProgressPercentage ? tr(STR_SHOW) : tr(STR_HIDE);
|
return SETTINGS.statusBarBookProgressPercentage ? tr(STR_SHOW) : tr(STR_HIDE);
|
||||||
case ITEM_PROGRESS_BAR:
|
case ITEM_PROGRESS_BAR:
|
||||||
|
|||||||
@@ -132,8 +132,7 @@ int UITheme::getStatusBarHeight() {
|
|||||||
|
|
||||||
// Add status bar margin
|
// Add status bar margin
|
||||||
const bool showStatusBar =
|
const bool showStatusBar =
|
||||||
SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::HIDE_PAGE_COUNT ||
|
SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
|
||||||
SETTINGS.statusBarBookProgressPercentage ||
|
|
||||||
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery ||
|
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery ||
|
||||||
SETTINGS.statusBarClock != CrossPointSettings::STATUS_BAR_CLOCK_MODE::STATUS_BAR_CLOCK_HIDE;
|
SETTINGS.statusBarClock != CrossPointSettings::STATUS_BAR_CLOCK_MODE::STATUS_BAR_CLOCK_HIDE;
|
||||||
const bool showProgressBar =
|
const bool showProgressBar =
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ constexpr int bookmarkStatusIconGap = 4;
|
|||||||
constexpr int bookmarkStatusIconTopCrop = 2;
|
constexpr int bookmarkStatusIconTopCrop = 2;
|
||||||
|
|
||||||
bool statusBarTextLaneVisible() {
|
bool statusBarTextLaneVisible() {
|
||||||
return SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::HIDE_PAGE_COUNT ||
|
return SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
|
||||||
SETTINGS.statusBarBookProgressPercentage ||
|
|
||||||
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery ||
|
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery ||
|
||||||
(SETTINGS.statusBarClock && halClock.isAvailable());
|
(SETTINGS.statusBarClock && halClock.isAvailable());
|
||||||
}
|
}
|
||||||
@@ -766,23 +765,20 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
|
|||||||
int leftClusterWidth = 0;
|
int leftClusterWidth = 0;
|
||||||
int rightClusterWidth = 0;
|
int rightClusterWidth = 0;
|
||||||
|
|
||||||
const bool showPageCount = SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::HIDE_PAGE_COUNT;
|
if (SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarChapterPageCount) {
|
||||||
if (SETTINGS.statusBarBookProgressPercentage || showPageCount) {
|
|
||||||
// Right aligned text for progress counter
|
// Right aligned text for progress counter
|
||||||
char progressStr[32];
|
char progressStr[32];
|
||||||
|
|
||||||
// Mark the total with "~" while it is only an estimate (a still-building spine's
|
// Prefix the page count with "~" while a still-building spine only yields an estimated total.
|
||||||
// watermark, or a whole-book total with not-yet-paginated chapters). The current
|
|
||||||
// page is always exact, so the marker sits on the total.
|
|
||||||
const char* estimatePrefix = pageCountEstimated ? "~" : "";
|
const char* estimatePrefix = pageCountEstimated ? "~" : "";
|
||||||
|
|
||||||
if (SETTINGS.statusBarBookProgressPercentage && showPageCount) {
|
if (SETTINGS.statusBarBookProgressPercentage && SETTINGS.statusBarChapterPageCount) {
|
||||||
snprintf(progressStr, sizeof(progressStr), "%d/%s%d %.0f%%", currentPage, estimatePrefix, pageCount,
|
snprintf(progressStr, sizeof(progressStr), "%s%d/%d %.0f%%", estimatePrefix, currentPage, pageCount,
|
||||||
bookProgress);
|
bookProgress);
|
||||||
} else if (SETTINGS.statusBarBookProgressPercentage) {
|
} else if (SETTINGS.statusBarBookProgressPercentage) {
|
||||||
snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress);
|
snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress);
|
||||||
} else {
|
} else {
|
||||||
snprintf(progressStr, sizeof(progressStr), "%d/%s%d", currentPage, estimatePrefix, pageCount);
|
snprintf(progressStr, sizeof(progressStr), "%s%d/%d", estimatePrefix, currentPage, pageCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
int progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);
|
int progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);
|
||||||
|
|||||||
@@ -200,6 +200,14 @@ void CrossPointWebServer::begin() {
|
|||||||
udpActive = udp.begin(LOCAL_UDP_PORT);
|
udpActive = udp.begin(LOCAL_UDP_PORT);
|
||||||
LOG_DBG("WEB", "Discovery UDP %s on port %d", udpActive ? "enabled" : "failed", LOCAL_UDP_PORT);
|
LOG_DBG("WEB", "Discovery UDP %s on port %d", udpActive ? "enabled" : "failed", LOCAL_UDP_PORT);
|
||||||
|
|
||||||
|
// All request handlers run on the task that calls handleClient(). Register
|
||||||
|
// that task before any handler can call esp_task_wdt_reset().
|
||||||
|
const esp_err_t watchdogResult = esp_task_wdt_add(nullptr);
|
||||||
|
watchdogTaskRegistered = watchdogResult == ESP_OK;
|
||||||
|
if (!watchdogTaskRegistered) {
|
||||||
|
LOG_ERR("WEB", "Failed to register web server task with watchdog: %s", esp_err_to_name(watchdogResult));
|
||||||
|
}
|
||||||
|
|
||||||
running = true;
|
running = true;
|
||||||
|
|
||||||
LOG_DBG("WEB", "Web server started on port %d", port);
|
LOG_DBG("WEB", "Web server started on port %d", port);
|
||||||
@@ -229,6 +237,10 @@ void CrossPointWebServer::abortWsUpload(const char* tag) {
|
|||||||
void CrossPointWebServer::stop() {
|
void CrossPointWebServer::stop() {
|
||||||
if (!running || !server) {
|
if (!running || !server) {
|
||||||
LOG_DBG("WEB", "stop() called but already stopped (running=%d, server=%p)", running, server.get());
|
LOG_DBG("WEB", "stop() called but already stopped (running=%d, server=%p)", running, server.get());
|
||||||
|
if (watchdogTaskRegistered) {
|
||||||
|
esp_task_wdt_delete(nullptr);
|
||||||
|
watchdogTaskRegistered = false;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +281,11 @@ void CrossPointWebServer::stop() {
|
|||||||
LOG_DBG("WEB", "Web server stopped and deleted");
|
LOG_DBG("WEB", "Web server stopped and deleted");
|
||||||
LOG_DBG("WEB", "[MEM] Free heap after delete server: %d bytes", ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap after delete server: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
|
if (watchdogTaskRegistered) {
|
||||||
|
esp_task_wdt_delete(nullptr);
|
||||||
|
watchdogTaskRegistered = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared
|
// Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared
|
||||||
// later in the file and will be cleared when they go out of scope or on next upload
|
// later in the file and will be cleared when they go out of scope or on next upload
|
||||||
LOG_DBG("WEB", "[MEM] Free heap final: %d bytes", ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap final: %d bytes", ESP.getFreeHeap());
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class CrossPointWebServer {
|
|||||||
std::unique_ptr<WebServer> server = nullptr;
|
std::unique_ptr<WebServer> server = nullptr;
|
||||||
std::unique_ptr<WebSocketsServer> wsServer = nullptr;
|
std::unique_ptr<WebSocketsServer> wsServer = nullptr;
|
||||||
bool running = false;
|
bool running = false;
|
||||||
|
bool watchdogTaskRegistered = false;
|
||||||
bool apMode = false; // true when running in AP mode, false for STA mode
|
bool apMode = false; // true when running in AP mode, false for STA mode
|
||||||
uint16_t port = 80;
|
uint16_t port = 80;
|
||||||
uint16_t wsPort = 81; // WebSocket port
|
uint16_t wsPort = 81; // WebSocket port
|
||||||
|
|||||||
Reference in New Issue
Block a user