Reduce memory footprint
This commit is contained in:
+68
-15
@@ -12,7 +12,7 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 26;
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 27;
|
||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
|
||||
sizeof(int) + // fontId
|
||||
sizeof(float) + // lineCompression
|
||||
@@ -20,6 +20,7 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
|
||||
sizeof(uint8_t) + // paragraphAlignment
|
||||
sizeof(uint16_t) + // viewportWidth
|
||||
sizeof(uint16_t) + // viewportHeight
|
||||
sizeof(bool) + // parseComplete
|
||||
sizeof(uint16_t) + // pageCount (stored as 16-bit in header)
|
||||
sizeof(bool) + // hyphenationEnabled
|
||||
sizeof(bool) + // embeddedStyle
|
||||
@@ -195,9 +196,9 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
}
|
||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
||||
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
|
||||
sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) + sizeof(imageRendering) +
|
||||
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||
sizeof(viewportHeight) + sizeof(bool) + sizeof(pageCount) +
|
||||
sizeof(hyphenationEnabled) + sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) +
|
||||
sizeof(imageRendering) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||
"Header size mismatch");
|
||||
serialization::writePod(file, SECTION_FILE_VERSION);
|
||||
serialization::writePod(file, fontId);
|
||||
@@ -210,6 +211,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
serialization::writePod(file, embeddedStyle);
|
||||
serialization::writePod(file, bionicReadingEnabled);
|
||||
serialization::writePod(file, imageRendering);
|
||||
serialization::writePod(file, false); // Placeholder for parseComplete (patched later)
|
||||
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
|
||||
@@ -220,13 +222,30 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const bool bionicReadingEnabled, const uint8_t imageRendering) {
|
||||
truncatedCache = false;
|
||||
uint32_t propertyHash =
|
||||
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering);
|
||||
filePath = getSectionFilePath(propertyHash);
|
||||
|
||||
bool usingEmbeddedStyleFallback = false;
|
||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||
return false;
|
||||
// Fallback: allow loading a no-CSS cache variant when embedded CSS is enabled.
|
||||
if (embeddedStyle) {
|
||||
const uint32_t fallbackHash =
|
||||
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, false, bionicReadingEnabled, imageRendering);
|
||||
const std::string fallbackPath = getSectionFilePath(fallbackHash);
|
||||
if (Storage.openFileForRead("SCT", fallbackPath, file)) {
|
||||
filePath = fallbackPath;
|
||||
usingEmbeddedStyleFallback = true;
|
||||
LOG_ERR("SCT", "Using no-CSS section cache fallback: %s", filePath.c_str());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Match parameters
|
||||
@@ -248,6 +267,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
bool fileEmbeddedStyle;
|
||||
bool fileBionicReadingEnabled;
|
||||
uint8_t fileImageRendering;
|
||||
bool fileParseComplete;
|
||||
serialization::readPod(file, fileFontId);
|
||||
serialization::readPod(file, fileLineCompression);
|
||||
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||
@@ -258,16 +278,21 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
serialization::readPod(file, fileEmbeddedStyle);
|
||||
serialization::readPod(file, fileBionicReadingEnabled);
|
||||
serialization::readPod(file, fileImageRendering);
|
||||
serialization::readPod(file, fileParseComplete);
|
||||
|
||||
const bool embeddedStyleMatches =
|
||||
(embeddedStyle == fileEmbeddedStyle) || (usingEmbeddedStyleFallback && !fileEmbeddedStyle);
|
||||
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
|
||||
hyphenationEnabled != fileHyphenationEnabled || !embeddedStyleMatches ||
|
||||
bionicReadingEnabled != fileBionicReadingEnabled || imageRendering != fileImageRendering) {
|
||||
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||
clearCache(); // closes file before removal
|
||||
return false;
|
||||
}
|
||||
|
||||
truncatedCache = !fileParseComplete;
|
||||
}
|
||||
|
||||
serialization::readPod(file, pageCount);
|
||||
@@ -311,6 +336,7 @@ bool Section::clearCache() {
|
||||
lut.clear();
|
||||
pageCount = 0;
|
||||
currentPage = 0;
|
||||
truncatedCache = false;
|
||||
|
||||
if (!Storage.exists(filePath.c_str())) {
|
||||
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
||||
@@ -413,7 +439,10 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
const uint32_t phaseParseStart = millis();
|
||||
const bool streamOk = epub->readItemContentsToStream(localPath, visitor, 1024);
|
||||
const bool finalizeOk = visitor.finalize();
|
||||
bool success = streamOk && finalizeOk && visitor.streamSucceeded();
|
||||
const bool parserStreamOk = visitor.streamSucceeded();
|
||||
const bool parseComplete = streamOk && finalizeOk && parserStreamOk;
|
||||
bool success = parseComplete;
|
||||
const bool hasParsedPages = pageCount > 0;
|
||||
const uint32_t parseMs = millis() - phaseParseStart;
|
||||
// streamMs is no longer a separate phase (SD-write of temp file is gone); keep the
|
||||
// log breakdown stable by reporting it as 0.
|
||||
@@ -421,13 +450,35 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
|
||||
const uint32_t phaseFinalizeStart = millis();
|
||||
if (!success) {
|
||||
LOG_ERR("SCT", "Failed to parse XML and build pages (stream=%d finalize=%d)", streamOk ? 1 : 0, finalizeOk ? 1 : 0);
|
||||
file.close();
|
||||
Storage.remove(filePath.c_str());
|
||||
if (cssParser) {
|
||||
cssParser->clear();
|
||||
// If parsing fails mid-stream due low memory but some pages were already serialized,
|
||||
// keep the partial section cache so the chapter remains readable instead of failing hard.
|
||||
if (hasParsedPages) {
|
||||
LOG_ERR("SCT", "Parse incomplete; keeping partial section cache with %u pages (stream=%d finalize=%d parser=%d)",
|
||||
pageCount, streamOk ? 1 : 0, finalizeOk ? 1 : 0, parserStreamOk ? 1 : 0);
|
||||
success = true;
|
||||
} else if (embeddedStyle) {
|
||||
LOG_ERR("SCT",
|
||||
"Parse failed with embedded CSS enabled; retrying section creation with embeddedStyle=0 "
|
||||
"(stream=%d finalize=%d)",
|
||||
streamOk ? 1 : 0, finalizeOk ? 1 : 0);
|
||||
file.close();
|
||||
Storage.remove(filePath.c_str());
|
||||
if (cssParser) {
|
||||
cssParser->clear();
|
||||
}
|
||||
return createSectionFile(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, false, bionicReadingEnabled, imageRendering,
|
||||
progressFn);
|
||||
} else {
|
||||
LOG_ERR("SCT", "Failed to parse XML and build pages (stream=%d finalize=%d)", streamOk ? 1 : 0,
|
||||
finalizeOk ? 1 : 0);
|
||||
file.close();
|
||||
Storage.remove(filePath.c_str());
|
||||
if (cssParser) {
|
||||
cssParser->clear();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const uint32_t fileSize = static_cast<uint32_t>(inflatedSize);
|
||||
|
||||
@@ -477,8 +528,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
serialization::writePod(file, entry.listItemIndex);
|
||||
}
|
||||
|
||||
// Patch header with final pageCount, lutOffset, anchorMapOffset, and paragraphLutOffset
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t) * 3 - sizeof(pageCount));
|
||||
// Patch header with final parseComplete/pageCount and offsets.
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t) * 3 - sizeof(pageCount) - sizeof(bool));
|
||||
serialization::writePod(file, parseComplete);
|
||||
serialization::writePod(file, pageCount);
|
||||
serialization::writePod(file, lutOffset);
|
||||
serialization::writePod(file, anchorMapOffset);
|
||||
@@ -499,6 +551,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
LOG_ERR("SCT", "Failed to open section file for reading after creation");
|
||||
return false;
|
||||
}
|
||||
truncatedCache = !parseComplete;
|
||||
this->lut = std::move(lut);
|
||||
const uint32_t finalizeMs = millis() - phaseFinalizeStart;
|
||||
const uint32_t totalMs = millis() - phaseTotalStart;
|
||||
|
||||
@@ -17,6 +17,7 @@ class Section {
|
||||
std::string filePath;
|
||||
FsFile file;
|
||||
std::vector<uint32_t> lut; // Cached page byte-offsets; loaded once, avoids per-page LUT seek
|
||||
bool truncatedCache = false;
|
||||
|
||||
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
|
||||
@@ -68,6 +69,7 @@ class Section {
|
||||
bool bionicReadingEnabled, uint8_t imageRendering,
|
||||
const std::function<void(int)>& progressFn = nullptr);
|
||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||
bool isTruncatedCache() const { return truncatedCache; }
|
||||
|
||||
// Given a page in this section, return the TOC index for that page.
|
||||
int getTocIndexForPage(int page) const;
|
||||
|
||||
@@ -26,6 +26,7 @@ struct JpegContext {
|
||||
const RenderConfig* config{nullptr};
|
||||
int screenWidth{0};
|
||||
int screenHeight{0};
|
||||
ImageDitherMode effectiveDitherMode{ImageDitherMode::Bayer};
|
||||
|
||||
// Source dimensions after JPEGDEC's built-in scaling
|
||||
int scaledSrcWidth{0};
|
||||
@@ -103,7 +104,7 @@ uint8_t ditherGray(JpegContext& ctx, uint8_t gray, int localX, int outX, int out
|
||||
return quantizeGray4Level(gray);
|
||||
}
|
||||
|
||||
switch (ctx.config->ditherMode) {
|
||||
switch (ctx.effectiveDitherMode) {
|
||||
case ImageDitherMode::Atkinson:
|
||||
if (ctx.atkinsonDitherer) {
|
||||
return ctx.atkinsonDitherer->processPixel(gray, localX);
|
||||
@@ -179,6 +180,71 @@ int32_t jpegSeek(JPEGFILE* pFile, int32_t pos) {
|
||||
constexpr size_t JPEG_DECODER_APPROX_SIZE = 20 * 1024;
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_JPEG = JPEG_DECODER_APPROX_SIZE + 16 * 1024;
|
||||
|
||||
// Optional memory-behavior knobs for embedded targets.
|
||||
#ifndef JPEG_ENABLE_FIRST_RENDER_NO_CACHE
|
||||
#define JPEG_ENABLE_FIRST_RENDER_NO_CACHE 1
|
||||
#endif
|
||||
|
||||
#ifndef JPEG_CACHE_MIN_FREE_HEAP_MARGIN
|
||||
#define JPEG_CACHE_MIN_FREE_HEAP_MARGIN (24 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef JPEG_CACHE_MIN_MAX_ALLOC_MARGIN
|
||||
#define JPEG_CACHE_MIN_MAX_ALLOC_MARGIN (20 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef JPEG_DITHER_LOW_MEM_MIN_FREE_HEAP
|
||||
#define JPEG_DITHER_LOW_MEM_MIN_FREE_HEAP (MIN_FREE_HEAP_FOR_JPEG + 8 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef JPEG_DITHER_LOW_MEM_MIN_MAX_ALLOC
|
||||
#define JPEG_DITHER_LOW_MEM_MIN_MAX_ALLOC (48 * 1024)
|
||||
#endif
|
||||
|
||||
size_t jpegCacheBytes(int width, int height) {
|
||||
return static_cast<size_t>((width + 3) / 4) * static_cast<size_t>(height);
|
||||
}
|
||||
|
||||
bool shouldEnableJpegCache(const RenderConfig& config, int width, int height) {
|
||||
if (config.cachePath.empty()) return false;
|
||||
|
||||
#if JPEG_ENABLE_FIRST_RENDER_NO_CACHE
|
||||
if (!Storage.exists(config.cachePath.c_str())) {
|
||||
LOG_DBG("JPG", "Skipping cache on first render (compile-time policy): %s", config.cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
const size_t cacheBytes = jpegCacheBytes(width, height);
|
||||
const size_t freeHeap = ESP.getFreeHeap();
|
||||
const size_t maxAlloc = ESP.getMaxAllocHeap();
|
||||
const size_t minFreeForCaching = MIN_FREE_HEAP_FOR_JPEG + JPEG_CACHE_MIN_FREE_HEAP_MARGIN;
|
||||
const size_t minMaxAllocForCaching = cacheBytes + JPEG_CACHE_MIN_MAX_ALLOC_MARGIN;
|
||||
|
||||
if (freeHeap < minFreeForCaching) {
|
||||
LOG_DBG("JPG", "Skipping cache: free heap %u < %u (cache %u bytes)", static_cast<unsigned>(freeHeap),
|
||||
static_cast<unsigned>(minFreeForCaching), static_cast<unsigned>(cacheBytes));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (maxAlloc < minMaxAllocForCaching) {
|
||||
LOG_DBG("JPG", "Skipping cache: max alloc %u < %u (cache %u bytes)", static_cast<unsigned>(maxAlloc),
|
||||
static_cast<unsigned>(minMaxAllocForCaching), static_cast<unsigned>(cacheBytes));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool shouldForceBayerDither(const RenderConfig& config) {
|
||||
if (!config.useDithering) return false;
|
||||
if (config.ditherMode == ImageDitherMode::Bayer) return false;
|
||||
|
||||
const size_t freeHeap = ESP.getFreeHeap();
|
||||
const size_t maxAlloc = ESP.getMaxAllocHeap();
|
||||
return freeHeap < JPEG_DITHER_LOW_MEM_MIN_FREE_HEAP || maxAlloc < JPEG_DITHER_LOW_MEM_MIN_MAX_ALLOC;
|
||||
}
|
||||
|
||||
bool readJpegDimensionsFromHeader(const std::string& imagePath, ImageDimensions& out) {
|
||||
FsFile f;
|
||||
if (!Storage.openFileForRead("JPG", imagePath, f)) {
|
||||
@@ -526,6 +592,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
ctx.config = &config;
|
||||
ctx.screenWidth = renderer.getScreenWidth();
|
||||
ctx.screenHeight = renderer.getScreenHeight();
|
||||
ctx.effectiveDitherMode = config.ditherMode;
|
||||
|
||||
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, jpegDrawCallback);
|
||||
if (rc != 1) {
|
||||
@@ -605,7 +672,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
jpeg->setUserPointer(&ctx);
|
||||
|
||||
// Allocate cache buffer using final output dimensions
|
||||
ctx.caching = !config.cachePath.empty();
|
||||
ctx.caching = shouldEnableJpegCache(config, destWidth, destHeight);
|
||||
if (ctx.caching) {
|
||||
if (!ctx.cache.allocate(destWidth, destHeight, config.x, config.y)) {
|
||||
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
|
||||
@@ -613,6 +680,12 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldForceBayerDither(config)) {
|
||||
LOG_DBG("JPG", "Low-memory mode: forcing Bayer dithering (%u free, %u max alloc)",
|
||||
static_cast<unsigned>(ESP.getFreeHeap()), static_cast<unsigned>(ESP.getMaxAllocHeap()));
|
||||
ctx.effectiveDitherMode = ImageDitherMode::Bayer;
|
||||
}
|
||||
|
||||
// See PngToFramebufferConverter for rationale: BW-only display needs a 1-bit
|
||||
// dither so mid-grays don't collapse to black under DirectPixelWriter's `< 3` rule.
|
||||
if (config.monochromeOutput) {
|
||||
@@ -624,7 +697,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
|
||||
if (config.useDithering && !ctx.atkinson1BitDitherer) {
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
switch (config.ditherMode) {
|
||||
switch (ctx.effectiveDitherMode) {
|
||||
case ImageDitherMode::Atkinson:
|
||||
ctx.atkinsonDitherer.reset(new (std::nothrow) AtkinsonDitherer(destWidth));
|
||||
if (!ctx.atkinsonDitherer) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <expat.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -30,11 +31,42 @@ constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||
constexpr size_t MIN_SIZE_FOR_POPUP = 15 * 1024;
|
||||
constexpr size_t SIZE_FOR_PROGRESS_HEARTBEAT = 30 * 1024;
|
||||
constexpr size_t SIZE_FOR_PROGRESS_FINE = 80 * 1024;
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_INDEXING_POPUP = 32 * 1024;
|
||||
constexpr size_t MIN_CONTIG_HEAP_FOR_INDEXING_POPUP = 12 * 1024;
|
||||
|
||||
// Parser progress popup/ticks trigger full-screen refreshes that can temporarily
|
||||
// collapse heap during section cache builds on constrained targets. Keep disabled
|
||||
// by default; enable only when debugging parse progress behavior.
|
||||
#ifndef EHP_ENABLE_PARSE_PROGRESS_UI
|
||||
#define EHP_ENABLE_PARSE_PROGRESS_UI 0
|
||||
#endif
|
||||
|
||||
constexpr size_t PARSE_BUFFER_SIZE = 1024;
|
||||
constexpr size_t IMAGE_EXTRACT_CHUNK_SIZE = 1024;
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_IMAGE_EXTRACT = 48 * 1024;
|
||||
constexpr size_t MIN_MAX_ALLOC_FOR_IMAGE_EXTRACT = 36 * 1024;
|
||||
|
||||
#ifndef EHP_TEXT_LAYOUT_SOFT_MIN_FREE_HEAP
|
||||
#define EHP_TEXT_LAYOUT_SOFT_MIN_FREE_HEAP (18 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef EHP_TEXT_LAYOUT_SOFT_MIN_MAX_ALLOC
|
||||
#define EHP_TEXT_LAYOUT_SOFT_MIN_MAX_ALLOC (12 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef EHP_TEXT_LAYOUT_HARD_MIN_FREE_HEAP
|
||||
#define EHP_TEXT_LAYOUT_HARD_MIN_FREE_HEAP (9 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef EHP_TEXT_LAYOUT_HARD_MIN_MAX_ALLOC
|
||||
#define EHP_TEXT_LAYOUT_HARD_MIN_MAX_ALLOC (6 * 1024)
|
||||
#endif
|
||||
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_TEXT_LAYOUT = EHP_TEXT_LAYOUT_SOFT_MIN_FREE_HEAP;
|
||||
constexpr size_t MIN_MAX_ALLOC_FOR_TEXT_LAYOUT = EHP_TEXT_LAYOUT_SOFT_MIN_MAX_ALLOC;
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_TEXT_LAYOUT_HARD = EHP_TEXT_LAYOUT_HARD_MIN_FREE_HEAP;
|
||||
constexpr size_t MIN_MAX_ALLOC_FOR_TEXT_LAYOUT_HARD = EHP_TEXT_LAYOUT_HARD_MIN_MAX_ALLOC;
|
||||
|
||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote", "pre"};
|
||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||
|
||||
@@ -198,6 +230,34 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ChapterHtmlSlimParser::ensureHeapForTextLayout(const char* phase) {
|
||||
if (streamFailed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t freeHeap = ESP.getFreeHeap();
|
||||
const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
|
||||
if (freeHeap >= MIN_FREE_HEAP_FOR_TEXT_LAYOUT || maxAllocHeap >= MIN_MAX_ALLOC_FOR_TEXT_LAYOUT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Soft low-memory zone: keep parsing in degraded mode and only hard-abort when
|
||||
// both free and contiguous heap fall to critical levels.
|
||||
if (freeHeap >= MIN_FREE_HEAP_FOR_TEXT_LAYOUT_HARD && maxAllocHeap >= MIN_MAX_ALLOC_FOR_TEXT_LAYOUT_HARD) {
|
||||
lowMemoryImageFallback = true;
|
||||
LOG_DBG("EHP", "Low heap (%u free, %u max alloc) before %s; continuing in degraded mode", freeHeap, maxAllocHeap,
|
||||
phase);
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_ERR("EHP", "Low heap (%u free, %u max alloc), aborting parse before %s", freeHeap, maxAllocHeap, phase);
|
||||
streamFailed = true;
|
||||
if (activeParser) {
|
||||
XML_StopParser(activeParser, XML_FALSE);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// flush the contents of partWordBuffer to currentTextBlock
|
||||
void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
// Determine font style from depth-based tracking and CSS effective style
|
||||
@@ -229,6 +289,11 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||
|
||||
if (currentTextBlock->size() > 96) {
|
||||
if (!ensureHeapForTextLayout("long-block split")) {
|
||||
partWordBufferIndex = 0;
|
||||
nextWordContinues = false;
|
||||
return;
|
||||
}
|
||||
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
||||
const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset();
|
||||
const uint16_t effectiveWidth =
|
||||
@@ -1572,8 +1637,25 @@ bool ChapterHtmlSlimParser::setup(const size_t totalInflatedSize) {
|
||||
progressStepPercent = 50;
|
||||
}
|
||||
|
||||
const uint32_t popupFreeHeap = ESP.getFreeHeap();
|
||||
const uint32_t popupContigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
#if EHP_ENABLE_PARSE_PROGRESS_UI
|
||||
progressUiEnabled =
|
||||
popupFreeHeap >= MIN_FREE_HEAP_FOR_INDEXING_POPUP && popupContigHeap >= MIN_CONTIG_HEAP_FOR_INDEXING_POPUP;
|
||||
if (!progressUiEnabled) {
|
||||
LOG_DBG("EHP", "Skipping indexing popup due to low heap (free=%u contig=%u)", popupFreeHeap, popupContigHeap);
|
||||
// When popup is disabled, also disable mid-parse ticks.
|
||||
progressStepPercent = 0;
|
||||
}
|
||||
#else
|
||||
progressUiEnabled = false;
|
||||
progressStepPercent = 0;
|
||||
LOG_DBG("EHP", "Skipping parser progress popup/ticks (EHP_ENABLE_PARSE_PROGRESS_UI=0, free=%u contig=%u)",
|
||||
popupFreeHeap, popupContigHeap);
|
||||
#endif
|
||||
|
||||
// Show initial progress popup for files above threshold.
|
||||
if (progressFn && totalStreamSize >= MIN_SIZE_FOR_POPUP) {
|
||||
if (progressFn && progressUiEnabled && totalStreamSize >= MIN_SIZE_FOR_POPUP) {
|
||||
progressFn(0);
|
||||
}
|
||||
return true;
|
||||
@@ -1614,7 +1696,7 @@ size_t ChapterHtmlSlimParser::write(const uint8_t* buffer, const size_t size) {
|
||||
// Report progress at the granularity chosen up-front (see progressStepPercent).
|
||||
// Skip the 100% callback — the page render that follows immediately replaces the popup,
|
||||
// so the final tick is wasted work.
|
||||
if (progressFn && progressStepPercent > 0 && totalStreamSize > 0) {
|
||||
if (progressFn && progressUiEnabled && progressStepPercent > 0 && totalStreamSize > 0) {
|
||||
const int progress = static_cast<int>(bytesStreamed * 100 / totalStreamSize);
|
||||
if (progress < 100 && progress / progressStepPercent > lastReportedProgress / progressStepPercent) {
|
||||
lastReportedProgress = progress;
|
||||
@@ -1736,6 +1818,10 @@ void ChapterHtmlSlimParser::makePages() {
|
||||
const uint16_t effectiveWidth =
|
||||
(horizontalInset < viewportWidth) ? static_cast<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
|
||||
|
||||
if (!ensureHeapForTextLayout("paragraph layout")) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentTextBlock->layoutAndExtractLines(
|
||||
renderer, fontId, effectiveWidth,
|
||||
[this](const std::shared_ptr<TextBlock>& textBlock, const bool lineEndsWithHyphenatedWord,
|
||||
|
||||
@@ -137,6 +137,7 @@ class ChapterHtmlSlimParser final : public Print {
|
||||
size_t bytesStreamed = 0;
|
||||
int lastReportedProgress = -1;
|
||||
int progressStepPercent = 0;
|
||||
bool progressUiEnabled = true;
|
||||
bool streamFailed = false;
|
||||
uint32_t streamStartTimeMs = 0;
|
||||
|
||||
@@ -155,6 +156,7 @@ class ChapterHtmlSlimParser final : public Print {
|
||||
std::unordered_map<std::string, CssStyle> inlineStyleCache_;
|
||||
|
||||
void updateEffectiveInlineStyle();
|
||||
bool ensureHeapForTextLayout(const char* phase);
|
||||
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||
void flushPartWordBuffer();
|
||||
void makePages();
|
||||
|
||||
@@ -44,6 +44,50 @@ constexpr unsigned long skipChapterMs = 700;
|
||||
// pages per minute, first item is 1 to prevent division by zero if accessed
|
||||
constexpr int PAGE_TURN_LABELS[] = {1, 1, 3, 6, 12};
|
||||
|
||||
// Silent next-chapter indexing should only run when heap is healthy enough,
|
||||
// otherwise it tends to produce heavily truncated fallback caches.
|
||||
#ifndef CP_SILENT_INDEX_MIN_FREE_HEAP_BYTES
|
||||
#define CP_SILENT_INDEX_MIN_FREE_HEAP_BYTES (64 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef CP_SILENT_INDEX_MIN_CONTIG_HEAP_BYTES
|
||||
#define CP_SILENT_INDEX_MIN_CONTIG_HEAP_BYTES (24 * 1024)
|
||||
#endif
|
||||
|
||||
constexpr uint32_t SILENT_INDEX_MIN_FREE_HEAP_BYTES = CP_SILENT_INDEX_MIN_FREE_HEAP_BYTES;
|
||||
constexpr uint32_t SILENT_INDEX_MIN_CONTIG_HEAP_BYTES = CP_SILENT_INDEX_MIN_CONTIG_HEAP_BYTES;
|
||||
|
||||
// Hysteresis for auto AA recovery after low-memory BW snapshot failures.
|
||||
// Override at build time via -D flags, for example:
|
||||
// -DCP_AA_RECOVERY_FREE_HEAP_BYTES=30720
|
||||
// -DCP_AA_RECOVERY_CONTIG_HEAP_BYTES=12288
|
||||
#ifndef CP_AA_RECOVERY_FREE_HEAP_BYTES
|
||||
#define CP_AA_RECOVERY_FREE_HEAP_BYTES (28 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef CP_AA_RECOVERY_CONTIG_HEAP_BYTES
|
||||
#define CP_AA_RECOVERY_CONTIG_HEAP_BYTES (12 * 1024)
|
||||
#endif
|
||||
|
||||
constexpr uint32_t AA_RECOVERY_FREE_HEAP_BYTES = CP_AA_RECOVERY_FREE_HEAP_BYTES;
|
||||
constexpr uint32_t AA_RECOVERY_CONTIG_HEAP_BYTES = CP_AA_RECOVERY_CONTIG_HEAP_BYTES;
|
||||
|
||||
// Snapshotting BW buffer for grayscale restore can fragment heap heavily on tight
|
||||
// pages. Skip attempting snapshot altogether below this safety window.
|
||||
#ifndef CP_BW_SNAPSHOT_MIN_FREE_HEAP_BYTES
|
||||
#define CP_BW_SNAPSHOT_MIN_FREE_HEAP_BYTES (72 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef CP_BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES
|
||||
#define CP_BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES (52 * 1024)
|
||||
#endif
|
||||
|
||||
constexpr uint32_t BW_SNAPSHOT_MIN_FREE_HEAP_BYTES = CP_BW_SNAPSHOT_MIN_FREE_HEAP_BYTES;
|
||||
constexpr uint32_t BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES = CP_BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES;
|
||||
constexpr uint8_t TRUNCATED_SECTION_HINT_RENDER_COUNT = 2;
|
||||
constexpr const char* TRUNCATED_SECTION_HINT_LINE_1 = "Chapter may be truncated (low memory).";
|
||||
constexpr const char* TRUNCATED_SECTION_HINT_LINE_2 = "Try: No embedded style | No images | AA Off";
|
||||
|
||||
#if DEBUG_MEMORY_CONSUMPTION
|
||||
void logReaderMemSnapshot(const char* stage) {
|
||||
const uint32_t freeHeap = esp_get_free_heap_size();
|
||||
@@ -1300,6 +1344,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
lastRenderStats.textAntiAliasing = SETTINGS.textAntiAliasing;
|
||||
lastRenderStats.freeHeapBefore = esp_get_free_heap_size();
|
||||
lastRenderStats.largestFreeBlockBefore = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
showTruncatedSectionHintThisRender = false;
|
||||
|
||||
if (!section) {
|
||||
if (currentSpineIndex < 0 || currentSpineIndex >= spineCount) {
|
||||
@@ -1353,6 +1398,12 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
}
|
||||
lastRenderStats.sectionLoadMs = millis() - sectionStart;
|
||||
|
||||
if (section->isTruncatedCache() && currentSpineIndex != lastWarnedTruncatedSpineIndex) {
|
||||
lastWarnedTruncatedSpineIndex = currentSpineIndex;
|
||||
truncatedSectionHintRendersRemaining = TRUNCATED_SECTION_HINT_RENDER_COUNT;
|
||||
LOG_INF("ERS", "Section %d is truncated; showing mitigation hint", currentSpineIndex);
|
||||
}
|
||||
|
||||
if (nextPageNumber == UINT16_MAX) {
|
||||
section->currentPage = section->pageCount - 1;
|
||||
} else {
|
||||
@@ -1472,10 +1523,14 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
lastRenderStats.spineIndex = currentSpineIndex;
|
||||
lastRenderStats.pageIndex = section->currentPage;
|
||||
lastRenderStats.pageCount = section->pageCount;
|
||||
showTruncatedSectionHintThisRender = truncatedSectionHintRendersRemaining > 0;
|
||||
|
||||
const auto start = millis();
|
||||
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||
lastRenderStats.requestRenderMs = millis() - start;
|
||||
if (truncatedSectionHintRendersRemaining > 0) {
|
||||
truncatedSectionHintRendersRemaining--;
|
||||
}
|
||||
LOG_DBG("ERS", "Rendered page in %dms", lastRenderStats.requestRenderMs);
|
||||
}
|
||||
silentIndexNextChapterIfNeeded(viewportWidth, viewportHeight);
|
||||
@@ -1495,6 +1550,13 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t freeHeap = esp_get_free_heap_size();
|
||||
const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
if (freeHeap < SILENT_INDEX_MIN_FREE_HEAP_BYTES || contigHeap < SILENT_INDEX_MIN_CONTIG_HEAP_BYTES) {
|
||||
LOG_DBG("ERS", "Skipping silent indexing due to low heap (free=%lu contig=%lu)", freeHeap, contigHeap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the next chapter cache while the penultimate page is on screen.
|
||||
if (section->currentPage != section->pageCount - 2) {
|
||||
return;
|
||||
@@ -1560,8 +1622,23 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
(int32_t)heapAfter - (int32_t)heapBefore);
|
||||
logReaderMemSnapshot("prewarm_end");
|
||||
|
||||
const bool aaConfigured = SETTINGS.textAntiAliasing;
|
||||
bool aaEnabledForThisRender = aaConfigured;
|
||||
if (aaConfigured && antiAliasingSuspendedLowMemory) {
|
||||
const uint32_t freeHeapNow = esp_get_free_heap_size();
|
||||
const uint32_t contigHeapNow = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
if (freeHeapNow >= AA_RECOVERY_FREE_HEAP_BYTES && contigHeapNow >= AA_RECOVERY_CONTIG_HEAP_BYTES) {
|
||||
antiAliasingSuspendedLowMemory = false;
|
||||
LOG_INF("ERS", "Re-enabling text anti-aliasing after heap recovery (free=%lu contig=%lu)", freeHeapNow,
|
||||
contigHeapNow);
|
||||
} else {
|
||||
aaEnabledForThisRender = false;
|
||||
}
|
||||
}
|
||||
lastRenderStats.textAntiAliasing = aaEnabledForThisRender;
|
||||
|
||||
// Force special handling for pages with images when anti-aliasing is on
|
||||
bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing;
|
||||
bool imagePageWithAA = page->hasImages() && aaEnabledForThisRender;
|
||||
bool forceHalfRefreshThisPage = pendingHalfRefreshAfterImagePage && SETTINGS.halfRefreshAfterImagePage;
|
||||
pendingHalfRefreshAfterImagePage = false;
|
||||
lastRenderStats.imagePageWithAA = imagePageWithAA;
|
||||
@@ -1570,6 +1647,23 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
logReaderMemSnapshot("before_bw_render");
|
||||
page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||
renderStatusBar();
|
||||
if (showTruncatedSectionHintThisRender) {
|
||||
const int hintX = orientedMarginLeft + 4;
|
||||
const int hintY1 = contentTop + 4;
|
||||
const int hintY2 = hintY1 + 20;
|
||||
const int maxWidth = std::max(0, renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight - 8);
|
||||
// Clear a dedicated band so the hint stays readable over any page content.
|
||||
const int boxX = hintX - 2;
|
||||
const int boxY = hintY1 - 2;
|
||||
const int boxW = maxWidth + 4;
|
||||
const int boxH = 44;
|
||||
renderer.fillRect(boxX, boxY, boxW, boxH, false);
|
||||
renderer.drawRect(boxX, boxY, boxW, boxH, true);
|
||||
const std::string line1 = renderer.truncatedText(UI_10_FONT_ID, TRUNCATED_SECTION_HINT_LINE_1, maxWidth);
|
||||
const std::string line2 = renderer.truncatedText(UI_10_FONT_ID, TRUNCATED_SECTION_HINT_LINE_2, maxWidth);
|
||||
renderer.drawText(UI_10_FONT_ID, hintX, hintY1, line1.c_str(), true, EpdFontFamily::BOLD);
|
||||
renderer.drawText(UI_10_FONT_ID, hintX, hintY2, line2.c_str(), true);
|
||||
}
|
||||
fcm->logStats("bw_render");
|
||||
const auto tBwRender = millis();
|
||||
logReaderMemSnapshot("after_bw_render");
|
||||
@@ -1604,11 +1698,33 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
}
|
||||
const auto tDisplay = millis();
|
||||
|
||||
// Save bw buffer to reset buffer state after grayscale data sync
|
||||
// Save bw buffer to reset buffer state after grayscale data sync.
|
||||
// Pre-check heap to avoid entering the chunk-retry path when success is unlikely.
|
||||
const uint32_t bwStoreFreeHeap = esp_get_free_heap_size();
|
||||
const uint32_t bwStoreContigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
const bool shouldAttemptBwSnapshot =
|
||||
bwStoreFreeHeap >= BW_SNAPSHOT_MIN_FREE_HEAP_BYTES && bwStoreContigHeap >= BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES;
|
||||
|
||||
logReaderMemSnapshot("bw_store_begin");
|
||||
renderer.storeBwBuffer();
|
||||
bool bwBufferStored = false;
|
||||
if (shouldAttemptBwSnapshot) {
|
||||
bwBufferStored = renderer.storeBwBuffer();
|
||||
} else {
|
||||
LOG_INF("ERS", "Skipping BW snapshot precheck (free=%lu contig=%lu, need free>=%lu contig>=%lu)", bwStoreFreeHeap,
|
||||
bwStoreContigHeap, static_cast<uint32_t>(BW_SNAPSHOT_MIN_FREE_HEAP_BYTES),
|
||||
static_cast<uint32_t>(BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES));
|
||||
}
|
||||
const auto tBwStore = millis();
|
||||
logReaderMemSnapshot("bw_store_end");
|
||||
if (!bwBufferStored) {
|
||||
if (aaEnabledForThisRender && !antiAliasingSuspendedLowMemory) {
|
||||
antiAliasingSuspendedLowMemory = true;
|
||||
const uint32_t freeHeapNow = esp_get_free_heap_size();
|
||||
const uint32_t contigHeapNow = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
LOG_INF("ERS", "Suspending text anti-aliasing due to low heap (free=%lu contig=%lu)", freeHeapNow, contigHeapNow);
|
||||
}
|
||||
LOG_INF("ERS", "Skipping grayscale/BW-restore for this page (insufficient heap for BW snapshot)");
|
||||
}
|
||||
|
||||
if (page->hasImages() && getEffectiveImageRendering() != CrossPointSettings::IMAGES_SUPPRESS) {
|
||||
pendingHalfRefreshAfterImagePage = true;
|
||||
@@ -1616,7 +1732,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
|
||||
// grayscale rendering
|
||||
// TODO: Only do this if font supports it
|
||||
if (SETTINGS.textAntiAliasing) {
|
||||
if (aaEnabledForThisRender && bwBufferStored) {
|
||||
logReaderMemSnapshot("gray_lsb_begin");
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
@@ -1659,21 +1775,24 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore,
|
||||
tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0);
|
||||
} else {
|
||||
// restore the bw data
|
||||
logReaderMemSnapshot("bw_restore_begin");
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
logReaderMemSnapshot("bw_restore_end");
|
||||
uint32_t bwRestoreMs = 0;
|
||||
if (bwBufferStored) {
|
||||
// restore the bw data
|
||||
logReaderMemSnapshot("bw_restore_begin");
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
logReaderMemSnapshot("bw_restore_end");
|
||||
bwRestoreMs = tBwRestore - tBwStore;
|
||||
}
|
||||
|
||||
const auto tEnd = millis();
|
||||
lastRenderStats.usedGrayscale = false;
|
||||
lastRenderStats.phases = {
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, 0, 0, 0, tBwRestore - tBwStore,
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, 0, 0, 0, bwRestoreMs,
|
||||
tEnd - t0};
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tBwRestore - tBwStore,
|
||||
tEnd - t0);
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, bwRestoreMs, tEnd - t0);
|
||||
}
|
||||
|
||||
if (const auto* cacheManager = renderer.getFontCacheManager()) {
|
||||
|
||||
@@ -40,6 +40,12 @@ class EpubReaderActivity final : public Activity {
|
||||
unsigned long lastPageTurnTime = 0UL;
|
||||
unsigned long pageTurnDuration = 0UL;
|
||||
bool pendingHalfRefreshAfterImagePage = false;
|
||||
// Temporary AA suspension when BW snapshot allocation fails under memory pressure.
|
||||
// Automatically lifted once heap recovers above hysteresis thresholds.
|
||||
bool antiAliasingSuspendedLowMemory = false;
|
||||
bool showTruncatedSectionHintThisRender = false;
|
||||
uint8_t truncatedSectionHintRendersRemaining = 0;
|
||||
int lastWarnedTruncatedSpineIndex = -1;
|
||||
struct RenderPhaseStats {
|
||||
unsigned long prewarmMs = 0UL;
|
||||
unsigned long bwRenderMs = 0UL;
|
||||
|
||||
Reference in New Issue
Block a user