fix: Add framebuffer release/realloc and improved lazy indexing (#2563)
This commit is contained in:
@@ -262,14 +262,42 @@ void EpubReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
// session, so reopening a partial deliberately does NOT start it (see the deferral in
|
||||
// render()); crossing this margin is the signal that the reader will actually need pages
|
||||
// past the watermark soon. Uses the last render's viewport so pagination matches the
|
||||
// partial being extended.
|
||||
if (section && !section->isBuilding() && section->isPartial() && !RenderLock::peek() && buildViewportWidth > 0 &&
|
||||
!partialRebuildStartFailed &&
|
||||
section->currentPage + PARTIAL_REBUILD_START_MARGIN >= static_cast<int>(section->pageCount)) {
|
||||
RenderLock lock;
|
||||
if (!section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, buildViewportWidth,
|
||||
buildViewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
|
||||
// Not fatal: the partial keeps serving its pages; crossing the watermark falls back to
|
||||
// the blocking extension in render(). Don't retry every tick.
|
||||
partialRebuildStartFailed = true;
|
||||
LOG_ERR("ERS", "Failed to start deferred partial extension build");
|
||||
} else {
|
||||
LOG_DBG("ERS", "Reader near partial watermark (%d/%d), resuming extension build", section->currentPage,
|
||||
section->pageCount);
|
||||
}
|
||||
}
|
||||
|
||||
// Drive any in-progress incremental section build forward, off the page-turn critical path,
|
||||
// but only within a small window ahead of the reader: an unbounded build monopolized the
|
||||
// RenderLock and locked out page turns. The build follows the reader instead, and instant
|
||||
// reopen comes from suspendBuild() persisting the laid-out pages as a partial on exit.
|
||||
// Skip while the render mutex is busy so we never delay a pending render; re-check
|
||||
// isBuilding() under the lock since render() may have just finished it.
|
||||
// While extending a partial (rebuild from a previous session), pageCount is pinned at the
|
||||
// partial's watermark until the build catches up, so the window check would wrongly read
|
||||
// "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.
|
||||
if (section && section->isBuilding() && !RenderLock::peek() &&
|
||||
static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD) {
|
||||
(section->isPartial() || static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD)) {
|
||||
RenderLock lock;
|
||||
// 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
|
||||
@@ -946,11 +974,17 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
|
||||
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
||||
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
||||
// Capture for loop()'s lazy partial-extension start (must match this render's layout params).
|
||||
buildViewportWidth = viewportWidth;
|
||||
buildViewportHeight = viewportHeight;
|
||||
|
||||
if (!section) {
|
||||
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
|
||||
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
|
||||
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
||||
// Fresh section, fresh chance: a failed lazy extension start in a previous
|
||||
// section must not suppress watermark-triggered rebuilds for this one.
|
||||
partialRebuildStartFailed = false;
|
||||
|
||||
// A finalized cache serves every page as-is. A partial cache (suspended build from a
|
||||
// previous session) serves its pages instantly too, but a build must still run to lay
|
||||
@@ -993,16 +1027,25 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
// The popup's own refresh is a plain FAST, so force the page that replaces it onto the HALF
|
||||
// ghost-cleanup path -- otherwise the "INDEXING" text ghosts under the rendered page.
|
||||
pagesUntilFullRefresh = 1;
|
||||
const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
|
||||
// No popup redraws while the framebuffer is lent to the build below;
|
||||
// the panel holds the popup displayed above (e-ink is persistent).
|
||||
const auto popupFn = [this]() {
|
||||
if (renderer.hasFrameBuffer()) GUI.drawPopup(renderer, tr(STR_INDEXING));
|
||||
};
|
||||
// Lend the framebuffer's 48 KB to the blocking full build; restored
|
||||
// (white) at scope exit, and the page render below redraws everything.
|
||||
GfxRenderer::FrameBufferLoan loan(renderer);
|
||||
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) {
|
||||
LOG_ERR("ERS", "Failed to persist page data to SD");
|
||||
section.reset();
|
||||
loan.end(); // restore before anything draws
|
||||
showBuildError();
|
||||
return;
|
||||
}
|
||||
loan.end();
|
||||
} else {
|
||||
// Lay out just enough to show the landing page; loop() builds the rest behind it. Show the
|
||||
// indexing popup up front only when the build will actually be slow: a large spine (its
|
||||
@@ -1010,52 +1053,73 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
// a deep resume/jump that must lay out many pages to reach the landing page. Tiny sections
|
||||
// build in a blink and stay popup-free.
|
||||
const int target = pendingPageJump.has_value() ? *pendingPageJump : (nextPageNumber < 0 ? 0 : nextPageNumber);
|
||||
const size_t spineBytes = epub->getCumulativeSpineItemSize(currentSpineIndex) -
|
||||
(currentSpineIndex > 0 ? epub->getCumulativeSpineItemSize(currentSpineIndex - 1) : 0);
|
||||
// Popup only when the build will actually be slow: a big spine whose HTML still needs
|
||||
// inflating (the multi-second cost), or a deep page target. A reopen with cached HTML builds
|
||||
// fast, so no popup -- that's what made an already-indexed book look like it was reindexing.
|
||||
// A partial cache that already covers the target page shows it instantly: never popup.
|
||||
const bool willInflate = !section->hasHtmlCache();
|
||||
const bool anchorJump = !pendingAnchor.empty();
|
||||
bool showPopup;
|
||||
if (anchorJump) {
|
||||
// An anchor jump's cost is bounded by the anchor's page, not `target`. An anchor already
|
||||
// in the on-disk map (partial or finalized cache) lands instantly: no popup. Otherwise it
|
||||
// lies beyond the indexed watermark and the build may lay out the whole spine to find it,
|
||||
// so gate on spine size alone -- laying out a big spine takes seconds even with cached
|
||||
// HTML. Ordinary chapter-top TOC jumps resolve on page 0 and stay popup-free.
|
||||
showPopup = !section->findAnchor(pendingAnchor).has_value() && spineBytes > BUILD_POPUP_BYTE_THRESHOLD;
|
||||
|
||||
// Landing well inside a partial: the page (or anchor, via the on-disk map) is already
|
||||
// servable, so don't restart the extension build now -- it re-lays out the WHOLE chapter
|
||||
// from page 0 (minutes of background CPU + SD writes on a giant spine), pure waste when
|
||||
// the reader never nears the watermark this session. loop() starts it lazily once the
|
||||
// reader is within PARTIAL_REBUILD_START_MARGIN pages of the watermark.
|
||||
if (section->isPartial() &&
|
||||
(anchorJump ? section->getPageForAnchor(pendingAnchor).has_value()
|
||||
: target + PARTIAL_REBUILD_START_MARGIN < static_cast<int>(section->pageCount))) {
|
||||
LOG_DBG("ERS", "Partial covers target %d of %d; deferring extension build", target, section->pageCount);
|
||||
} else {
|
||||
const bool targetAvailable = target < static_cast<int>(section->pageCount);
|
||||
showPopup = !targetAvailable &&
|
||||
((spineBytes > BUILD_POPUP_BYTE_THRESHOLD && willInflate) || target > BUILD_POPUP_PAGE_THRESHOLD);
|
||||
}
|
||||
if (showPopup) {
|
||||
GUI.drawPopup(renderer, tr(STR_INDEXING));
|
||||
// HALF-clear the popup when the page replaces it, else "INDEXING" ghosts under the page.
|
||||
pagesUntilFullRefresh = 1;
|
||||
}
|
||||
if (!section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
|
||||
LOG_ERR("ERS", "Failed to start section build");
|
||||
section.reset();
|
||||
showBuildError();
|
||||
return;
|
||||
}
|
||||
while (!section->isBuildComplete() &&
|
||||
(anchorJump ? !section->findAnchor(pendingAnchor) : static_cast<int>(section->pageCount) <= target)) {
|
||||
// Anchor jump: build until the anchor's page is laid out (usually page 0), checking a
|
||||
// partial's on-disk anchor map too so an already-indexed anchor resolves immediately.
|
||||
// Otherwise: build until the target page exists. loop() builds the rest behind it.
|
||||
if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) {
|
||||
LOG_ERR("ERS", "Failed during incremental section build");
|
||||
const size_t spineBytes =
|
||||
epub->getCumulativeSpineItemSize(currentSpineIndex) -
|
||||
(currentSpineIndex > 0 ? epub->getCumulativeSpineItemSize(currentSpineIndex - 1) : 0);
|
||||
// Popup only when the build will actually be slow: a big spine whose HTML still needs
|
||||
// inflating (the multi-second cost), or a deep page target. A reopen with cached HTML builds
|
||||
// fast, so no popup -- that's what made an already-indexed book look like it was reindexing.
|
||||
// A partial cache that already covers the target page shows it instantly: never popup.
|
||||
const bool willInflate = !section->hasHtmlCache();
|
||||
bool showPopup;
|
||||
if (anchorJump) {
|
||||
// An anchor jump's cost is bounded by the anchor's page, not `target`. An anchor already
|
||||
// in the on-disk map (partial or finalized cache) lands instantly: no popup. Otherwise it
|
||||
// lies beyond the indexed watermark and the build may lay out the whole spine to find it,
|
||||
// so gate on spine size alone -- laying out a big spine takes seconds even with cached
|
||||
// HTML. Ordinary chapter-top TOC jumps resolve on page 0 and stay popup-free.
|
||||
showPopup = !section->findAnchor(pendingAnchor).has_value() && spineBytes > BUILD_POPUP_BYTE_THRESHOLD;
|
||||
} else {
|
||||
const bool targetAvailable = target < static_cast<int>(section->pageCount);
|
||||
showPopup = !targetAvailable && ((spineBytes > BUILD_POPUP_BYTE_THRESHOLD && willInflate) ||
|
||||
target > BUILD_POPUP_PAGE_THRESHOLD);
|
||||
}
|
||||
if (showPopup) {
|
||||
GUI.drawPopup(renderer, tr(STR_INDEXING));
|
||||
// HALF-clear the popup when the page replaces it, else "INDEXING" ghosts under the page.
|
||||
pagesUntilFullRefresh = 1;
|
||||
}
|
||||
// Lend the framebuffer's 48 KB to the blocking pre-render burst
|
||||
// (startBuild inflates the whole spine HTML — the memory peak). The
|
||||
// background buildSomeMore chunks in loop() do NOT get the loan: they
|
||||
// deliberately interleave with page renders. Restored before render.
|
||||
GfxRenderer::FrameBufferLoan loan(renderer);
|
||||
if (!section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
|
||||
LOG_ERR("ERS", "Failed to start section build");
|
||||
section.reset();
|
||||
loan.end(); // restore before anything draws (showBuildError renders a popup)
|
||||
showBuildError();
|
||||
return;
|
||||
}
|
||||
while (!section->isBuildComplete() &&
|
||||
(anchorJump ? !section->findAnchor(pendingAnchor) : static_cast<int>(section->pageCount) <= target)) {
|
||||
// Anchor jump: build until the anchor's page is laid out (usually page 0), checking a
|
||||
// partial's on-disk anchor map too so an already-indexed anchor resolves immediately.
|
||||
// Otherwise: build until the target page exists. loop() builds the rest behind it.
|
||||
if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) {
|
||||
LOG_ERR("ERS", "Failed during incremental section build");
|
||||
section.reset();
|
||||
loan.end(); // restore before anything draws (showBuildError renders a popup)
|
||||
showBuildError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
loan.end();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1098,6 +1162,16 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
// Extend the build to the requested page if needed (for partials and in-progress builds).
|
||||
// This runs every render, so it covers both the first page and any forward turn that gets
|
||||
// ahead of the background builder; pages already built do no work here.
|
||||
//
|
||||
// Crossing a partial's watermark before the extension rebuild has caught up means a
|
||||
// synchronous wait spanning the remaining prefix re-layout -- potentially tens of
|
||||
// seconds on a giant spine. Show the indexing popup so it isn't a silent freeze
|
||||
// (the page that replaces it takes the HALF ghost-cleanup path). Ordinary window
|
||||
// catch-ups on a non-partial build are a page or two and stay popup-free.
|
||||
if (section->isPartial() && section->currentPage >= static_cast<int>(section->pageCount)) {
|
||||
GUI.drawPopup(renderer, tr(STR_INDEXING));
|
||||
pagesUntilFullRefresh = 1;
|
||||
}
|
||||
while (section->isPartial() && section->currentPage >= static_cast<int>(section->pageCount)) {
|
||||
// Start a build to extend a partial toward the requested page.
|
||||
if (!section->isBuilding() &&
|
||||
|
||||
@@ -63,6 +63,15 @@ class EpubReaderActivity final : public Activity {
|
||||
SavedPosition savedPositions[MAX_FOOTNOTE_DEPTH] = {};
|
||||
int footnoteDepth = 0;
|
||||
|
||||
// Viewport of the last render(), captured so loop()'s lazy partial-extension start
|
||||
// builds with IDENTICAL layout parameters to the pages already rendered (a mismatch
|
||||
// would paginate differently than the partial being extended). 0 = no render yet.
|
||||
uint16_t buildViewportWidth = 0;
|
||||
uint16_t buildViewportHeight = 0;
|
||||
// Set when the lazy extension start failed, so loop() doesn't retry (and log) every
|
||||
// tick; the blocking extension in render() remains the fallback past the watermark.
|
||||
bool partialRebuildStartFailed = false;
|
||||
|
||||
// Last position persisted by render()'s saveProgress, used to skip redundant
|
||||
// writeAtomic calls on no-op re-renders (menu/bookmark/screenshot).
|
||||
int lastSavedSpineIndex = -1;
|
||||
@@ -85,6 +94,13 @@ class EpubReaderActivity final : public Activity {
|
||||
// in one sitting -- instant reopen comes from Section::suspendBuild() persisting the pages
|
||||
// already laid out as a partial file on exit/sleep.
|
||||
static constexpr int BUILD_WINDOW_AHEAD = 5;
|
||||
// Reopening a partial does NOT immediately restart its extension build (a whole-chapter
|
||||
// re-layout from page 0 -- minutes of background CPU + SD writes on a giant spine, wasted
|
||||
// when the reader never crosses the watermark that session). Instead loop() starts it once
|
||||
// the reader is within this many pages of the watermark: at ~30s per page read and ~100-300ms
|
||||
// per page rebuilt, this margin gives the rebuild ample runway to catch up (and finalize)
|
||||
// before the reader arrives.
|
||||
static constexpr int PARTIAL_REBUILD_START_MARGIN = 15;
|
||||
// Show the indexing popup when an initial build must lay out more than this many pages up front
|
||||
// (a deep resume/jump into a not-yet-built section), so it isn't a silent wait. Kept independent
|
||||
// of the small look-ahead window so ordinary landings stay popup-free.
|
||||
@@ -124,6 +140,11 @@ class EpubReaderActivity final : public Activity {
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&& lock) override;
|
||||
// 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
|
||||
// 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.
|
||||
bool skipLoopDelay() override { return section && section->isBuilding(); }
|
||||
bool isReaderActivity() const override { return true; }
|
||||
ScreenshotInfo getScreenshotInfo() const override;
|
||||
CrossPointPosition getCurrentPosition() const;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <I18n.h>
|
||||
#include <Memory.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "Epub.h"
|
||||
#include "EpubReaderActivity.h"
|
||||
@@ -40,10 +42,20 @@ std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
|
||||
// First open: building the spine/TOC index (book.bin) takes a couple of seconds. Show the
|
||||
// indexing popup so it isn't a silent wait on the home screen. The cachePath/hash is known at
|
||||
// construction, so this check is valid before load(); a cached open loads in a blink -> no popup.
|
||||
if (!Storage.exists((epub->getCachePath() + "/book.bin").c_str())) {
|
||||
const bool uncached = !Storage.exists((epub->getCachePath() + "/book.bin").c_str());
|
||||
if (uncached) {
|
||||
GUI.drawPopup(renderer, tr(STR_INDEXING));
|
||||
}
|
||||
if (epub->load(true, SETTINGS.embeddedStyle == 0)) {
|
||||
bool loaded;
|
||||
{
|
||||
// Lend the framebuffer's 48 KB to the container parse (expat + spine/TOC
|
||||
// build). The popup just displayed stays on the panel; whichever reader
|
||||
// activity follows redraws the full screen anyway.
|
||||
std::optional<GfxRenderer::FrameBufferLoan> loan;
|
||||
if (uncached) loan.emplace(renderer);
|
||||
loaded = epub->load(true, SETTINGS.embeddedStyle == 0);
|
||||
}
|
||||
if (loaded) {
|
||||
return epub;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user