Implement sparse css cache plus dynamic banding for AA
This commit is contained in:
+11
-5
@@ -454,6 +454,11 @@ void Epub::parseCssFiles() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No cache yet - parse CSS files
|
// No cache yet - parse CSS files
|
||||||
|
if (!cssParser->beginCacheCompile()) {
|
||||||
|
LOG_ERR("EBP", "Failed to start CSS compile pipeline");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& cssPath : cssFiles) {
|
for (const auto& cssPath : cssFiles) {
|
||||||
LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str());
|
LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str());
|
||||||
|
|
||||||
@@ -496,16 +501,17 @@ void Epub::parseCssFiles() const {
|
|||||||
Storage.remove(tmpCssPath.c_str());
|
Storage.remove(tmpCssPath.c_str());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
cssParser->loadFromStream(tempCssFile);
|
if (!cssParser->appendCompiledFromStream(tempCssFile)) {
|
||||||
|
LOG_ERR("EBP", "Failed to compile CSS file: %s", cssPath.c_str());
|
||||||
|
}
|
||||||
tempCssFile.close();
|
tempCssFile.close();
|
||||||
Storage.remove(tmpCssPath.c_str());
|
Storage.remove(tmpCssPath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to cache for next time
|
// Finalize compact cache for next time.
|
||||||
if (!cssParser->saveToCache()) {
|
if (!cssParser->endCacheCompile()) {
|
||||||
LOG_ERR("EBP", "Failed to save CSS rules to cache");
|
LOG_ERR("EBP", "Failed to finalize CSS rules cache");
|
||||||
}
|
}
|
||||||
cssParser->clear();
|
|
||||||
|
|
||||||
LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size());
|
LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
|
#include <esp_system.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@@ -46,6 +48,20 @@ namespace {
|
|||||||
constexpr uint32_t FNV_PRIME = 0x01000193; // 16777619
|
constexpr uint32_t FNV_PRIME = 0x01000193; // 16777619
|
||||||
constexpr uint32_t FNV_OFFSET_BASIS = 0x811C9DC5; // 2166136261
|
constexpr uint32_t FNV_OFFSET_BASIS = 0x811C9DC5; // 2166136261
|
||||||
|
|
||||||
|
// On constrained targets, loading the CSS rules map before chapter parsing can
|
||||||
|
// consume a large share of available heap and increase parse truncation risk.
|
||||||
|
// Allow compile-time override for tuning.
|
||||||
|
#ifndef SCT_EMBEDDED_STYLE_MIN_FREE_HEAP_BYTES
|
||||||
|
#define SCT_EMBEDDED_STYLE_MIN_FREE_HEAP_BYTES (96 * 1024)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SCT_EMBEDDED_STYLE_MIN_CONTIG_HEAP_BYTES
|
||||||
|
#define SCT_EMBEDDED_STYLE_MIN_CONTIG_HEAP_BYTES (56 * 1024)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
constexpr uint32_t EMBEDDED_STYLE_MIN_FREE_HEAP_BYTES = SCT_EMBEDDED_STYLE_MIN_FREE_HEAP_BYTES;
|
||||||
|
constexpr uint32_t EMBEDDED_STYLE_MIN_CONTIG_HEAP_BYTES = SCT_EMBEDDED_STYLE_MIN_CONTIG_HEAP_BYTES;
|
||||||
|
|
||||||
uint32_t fnv1a(const uint8_t* data, size_t length) {
|
uint32_t fnv1a(const uint8_t* data, size_t length) {
|
||||||
uint32_t hash = FNV_OFFSET_BASIS;
|
uint32_t hash = FNV_OFFSET_BASIS;
|
||||||
for (size_t i = 0; i < length; ++i) {
|
for (size_t i = 0; i < length; ++i) {
|
||||||
@@ -357,6 +373,21 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||||
const bool bionicReadingEnabled, const uint8_t imageRendering,
|
const bool bionicReadingEnabled, const uint8_t imageRendering,
|
||||||
const std::function<void(int)>& progressFn) {
|
const std::function<void(int)>& progressFn) {
|
||||||
|
if (embeddedStyle) {
|
||||||
|
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 < EMBEDDED_STYLE_MIN_FREE_HEAP_BYTES || contigHeap < EMBEDDED_STYLE_MIN_CONTIG_HEAP_BYTES) {
|
||||||
|
LOG_INF("SCT",
|
||||||
|
"Low heap for embedded CSS (free=%lu contig=%lu, need free>=%lu contig>=%lu); "
|
||||||
|
"building no-CSS section cache",
|
||||||
|
freeHeap, contigHeap, static_cast<uint32_t>(EMBEDDED_STYLE_MIN_FREE_HEAP_BYTES),
|
||||||
|
static_cast<uint32_t>(EMBEDDED_STYLE_MIN_CONTIG_HEAP_BYTES));
|
||||||
|
return createSectionFile(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
|
viewportHeight, hyphenationEnabled, false, bionicReadingEnabled, imageRendering,
|
||||||
|
progressFn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t propertyHash =
|
uint32_t propertyHash =
|
||||||
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering);
|
viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering);
|
||||||
@@ -401,6 +432,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
if (!cssParser->loadFromCache()) {
|
if (!cssParser->loadFromCache()) {
|
||||||
LOG_ERR("SCT", "Failed to load CSS from cache");
|
LOG_ERR("SCT", "Failed to load CSS from cache");
|
||||||
}
|
}
|
||||||
|
cssParser->resetResolveStats();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,6 +472,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
const bool streamOk = epub->readItemContentsToStream(localPath, visitor, 1024);
|
const bool streamOk = epub->readItemContentsToStream(localPath, visitor, 1024);
|
||||||
const bool finalizeOk = visitor.finalize();
|
const bool finalizeOk = visitor.finalize();
|
||||||
const bool parserStreamOk = visitor.streamSucceeded();
|
const bool parserStreamOk = visitor.streamSucceeded();
|
||||||
|
if (cssParser) {
|
||||||
|
cssParser->logResolveStats(localPath.c_str());
|
||||||
|
}
|
||||||
const bool parseComplete = streamOk && finalizeOk && parserStreamOk;
|
const bool parseComplete = streamOk && finalizeOk && parserStreamOk;
|
||||||
bool success = parseComplete;
|
bool success = parseComplete;
|
||||||
const bool hasParsedPages = pageCount > 0;
|
const bool hasParsedPages = pageCount > 0;
|
||||||
|
|||||||
+482
-180
@@ -45,10 +45,32 @@ constexpr size_t MAX_RULES = 1500;
|
|||||||
// If below this threshold, we skip CSS to avoid display artifacts.
|
// If below this threshold, we skip CSS to avoid display artifacts.
|
||||||
constexpr size_t MIN_FREE_HEAP_FOR_CSS = 48 * 1024;
|
constexpr size_t MIN_FREE_HEAP_FOR_CSS = 48 * 1024;
|
||||||
|
|
||||||
|
// In-memory CSS rule cache sizing for disk-backed lookup mode.
|
||||||
|
// Keeps memory bounded on large books while retaining hot selectors.
|
||||||
|
#ifndef CSS_HOT_RULE_CACHE_SIZE
|
||||||
|
#define CSS_HOT_RULE_CACHE_SIZE 128
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CSS_NEGATIVE_CACHE_SIZE
|
||||||
|
#define CSS_NEGATIVE_CACHE_SIZE 256
|
||||||
|
#endif
|
||||||
|
|
||||||
|
constexpr size_t HOT_RULE_CACHE_SIZE = CSS_HOT_RULE_CACHE_SIZE;
|
||||||
|
constexpr size_t NEGATIVE_CACHE_SIZE = CSS_NEGATIVE_CACHE_SIZE;
|
||||||
|
|
||||||
// Maximum length for a single selector string
|
// Maximum length for a single selector string
|
||||||
// Prevents parsing of extremely long or malformed selectors
|
// Prevents parsing of extremely long or malformed selectors
|
||||||
constexpr size_t MAX_SELECTOR_LENGTH = 256;
|
constexpr size_t MAX_SELECTOR_LENGTH = 256;
|
||||||
|
|
||||||
|
constexpr size_t CSS_LENGTH_FIELD_COUNT = 11;
|
||||||
|
constexpr size_t CSS_LENGTH_BYTES = sizeof(float) + sizeof(uint8_t);
|
||||||
|
constexpr size_t CSS_FIXED_STYLE_BYTES =
|
||||||
|
4 * sizeof(uint8_t) + (CSS_LENGTH_FIELD_COUNT * CSS_LENGTH_BYTES) + sizeof(uint8_t) + sizeof(uint16_t);
|
||||||
|
|
||||||
|
// Cache file name (version is CssParser::CSS_CACHE_VERSION)
|
||||||
|
constexpr char rulesCache[] = "/css_rules.cache";
|
||||||
|
constexpr char compileTempRulesCache[] = "/css_rules.compile.tmp";
|
||||||
|
|
||||||
// Check if character is CSS whitespace
|
// Check if character is CSS whitespace
|
||||||
bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; }
|
bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; }
|
||||||
|
|
||||||
@@ -445,12 +467,40 @@ void CssParser::processRuleBlockWithStyle(const std::string& selectorGroup, cons
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip if this would exceed the rule limit
|
// Skip if this would exceed the rule limit
|
||||||
if (rulesBySelector_.size() >= MAX_RULES) {
|
const size_t ruleCount = compileModeActive_ ? compileSelectorOffsets_.size() : rulesBySelector_.size();
|
||||||
|
if (ruleCount >= MAX_RULES) {
|
||||||
LOG_DBG("CSS", "Reached max rules limit, stopping selector processing");
|
LOG_DBG("CSS", "Reached max rules limit, stopping selector processing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store or merge with existing
|
if (compileModeActive_) {
|
||||||
|
if (!compileTempFile_) {
|
||||||
|
compileModeFailed_ = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CssStyle merged = style;
|
||||||
|
auto existingOffsetIt = compileSelectorOffsets_.find(key);
|
||||||
|
if (existingOffsetIt != compileSelectorOffsets_.end()) {
|
||||||
|
CssStyle existing;
|
||||||
|
FsFile tempRead;
|
||||||
|
if (Storage.openFileForRead("CSS", compileTempPath_, tempRead) && tempRead.seek(existingOffsetIt->second) &&
|
||||||
|
readCssStylePayload(tempRead, existing)) {
|
||||||
|
existing.applyOver(merged);
|
||||||
|
merged = existing;
|
||||||
|
}
|
||||||
|
if (tempRead) {
|
||||||
|
tempRead.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t styleOffset = compileTempFile_.position();
|
||||||
|
writeCssStylePayload(compileTempFile_, merged);
|
||||||
|
compileSelectorOffsets_[key] = styleOffset;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store or merge with existing (non-compile mode)
|
||||||
auto it = rulesBySelector_.find(key);
|
auto it = rulesBySelector_.find(key);
|
||||||
if (it != rulesBySelector_.end()) {
|
if (it != rulesBySelector_.end()) {
|
||||||
it->second.applyOver(style);
|
it->second.applyOver(style);
|
||||||
@@ -608,25 +658,432 @@ bool CssParser::loadFromStream(FsFile& source) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CssParser::beginCacheCompile() {
|
||||||
|
clear();
|
||||||
|
compileTempPath_ = cachePath + compileTempRulesCache;
|
||||||
|
Storage.remove(compileTempPath_.c_str());
|
||||||
|
if (!Storage.openFileForWrite("CSS", compileTempPath_, compileTempFile_)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
compileSelectorOffsets_.clear();
|
||||||
|
compileModeActive_ = true;
|
||||||
|
compileModeFailed_ = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::appendCompiledFromStream(FsFile& source) {
|
||||||
|
if (!compileModeActive_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!loadFromStream(source)) {
|
||||||
|
compileModeFailed_ = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !compileModeFailed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::endCacheCompile() {
|
||||||
|
if (!compileModeActive_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
compileModeActive_ = false;
|
||||||
|
compileTempFile_.close();
|
||||||
|
|
||||||
|
if (compileModeFailed_) {
|
||||||
|
Storage.remove(compileTempPath_.c_str());
|
||||||
|
compileSelectorOffsets_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile outFile;
|
||||||
|
if (!Storage.openFileForWrite("CSS", cachePath + rulesCache, outFile)) {
|
||||||
|
Storage.remove(compileTempPath_.c_str());
|
||||||
|
compileSelectorOffsets_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile.write(CssParser::CSS_CACHE_VERSION);
|
||||||
|
const auto ruleCount = static_cast<uint16_t>(compileSelectorOffsets_.size());
|
||||||
|
outFile.write(reinterpret_cast<const uint8_t*>(&ruleCount), sizeof(ruleCount));
|
||||||
|
|
||||||
|
FsFile tempFile;
|
||||||
|
if (!Storage.openFileForRead("CSS", compileTempPath_, tempFile)) {
|
||||||
|
outFile.close();
|
||||||
|
Storage.remove((cachePath + rulesCache).c_str());
|
||||||
|
Storage.remove(compileTempPath_.c_str());
|
||||||
|
compileSelectorOffsets_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<uint8_t, CSS_FIXED_STYLE_BYTES> styleBytes{};
|
||||||
|
for (const auto& it : compileSelectorOffsets_) {
|
||||||
|
const auto selectorLen = static_cast<uint16_t>(it.first.size());
|
||||||
|
outFile.write(reinterpret_cast<const uint8_t*>(&selectorLen), sizeof(selectorLen));
|
||||||
|
outFile.write(reinterpret_cast<const uint8_t*>(it.first.data()), selectorLen);
|
||||||
|
|
||||||
|
if (!tempFile.seek(it.second)) {
|
||||||
|
tempFile.close();
|
||||||
|
outFile.close();
|
||||||
|
Storage.remove((cachePath + rulesCache).c_str());
|
||||||
|
Storage.remove(compileTempPath_.c_str());
|
||||||
|
compileSelectorOffsets_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tempFile.read(styleBytes.data(), styleBytes.size()) != static_cast<int>(styleBytes.size())) {
|
||||||
|
tempFile.close();
|
||||||
|
outFile.close();
|
||||||
|
Storage.remove((cachePath + rulesCache).c_str());
|
||||||
|
Storage.remove(compileTempPath_.c_str());
|
||||||
|
compileSelectorOffsets_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outFile.write(styleBytes.data(), styleBytes.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFile.close();
|
||||||
|
outFile.close();
|
||||||
|
Storage.remove(compileTempPath_.c_str());
|
||||||
|
|
||||||
|
compileSelectorOffsets_.clear();
|
||||||
|
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
hotRuleCache_.clear();
|
||||||
|
hotRuleLru_.clear();
|
||||||
|
negativeRuleCache_.clear();
|
||||||
|
cacheRuleOffsets_.clear();
|
||||||
|
cacheIndexLoaded_ = false;
|
||||||
|
cachedRuleCount_ = 0;
|
||||||
|
|
||||||
|
return ensureCacheIndexLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::empty() const { return ruleCount() == 0; }
|
||||||
|
|
||||||
|
size_t CssParser::ruleCount() const {
|
||||||
|
if (!rulesBySelector_.empty()) {
|
||||||
|
return rulesBySelector_.size();
|
||||||
|
}
|
||||||
|
if (cacheIndexLoaded_) {
|
||||||
|
return cachedRuleCount_;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CssParser::clear() {
|
||||||
|
if (compileTempFile_) {
|
||||||
|
compileTempFile_.close();
|
||||||
|
}
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
cacheRuleOffsets_.clear();
|
||||||
|
hotRuleCache_.clear();
|
||||||
|
hotRuleLru_.clear();
|
||||||
|
negativeRuleCache_.clear();
|
||||||
|
cacheIndexLoaded_ = false;
|
||||||
|
cachedRuleCount_ = 0;
|
||||||
|
resolveStats_ = {};
|
||||||
|
compileModeActive_ = false;
|
||||||
|
compileModeFailed_ = false;
|
||||||
|
compileSelectorOffsets_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CssParser::resetResolveStats() const { resolveStats_ = {}; }
|
||||||
|
|
||||||
|
CssParser::ResolveStats CssParser::getResolveStats() const { return resolveStats_; }
|
||||||
|
|
||||||
|
void CssParser::logResolveStats(const char* context) const {
|
||||||
|
const auto s = getResolveStats();
|
||||||
|
LOG_DBG("CSS",
|
||||||
|
"resolve stats[%s]: calls=%lu lowHeapSkips=%lu mapHits=%lu hotHits=%lu diskHits=%lu misses=%lu "
|
||||||
|
"negativeHits=%lu hotSize=%u indexSize=%u",
|
||||||
|
context ? context : "n/a", s.resolveCalls, s.lowHeapSkips, s.mapHits, s.hotHits, s.diskHits, s.misses,
|
||||||
|
s.negativeHits, static_cast<unsigned>(hotRuleCache_.size()), static_cast<unsigned>(cachedRuleCount_));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::readCssStylePayload(FsFile& file, CssStyle& style) {
|
||||||
|
uint8_t enumVal;
|
||||||
|
if (file.read(&enumVal, 1) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.textAlign = static_cast<CssTextAlign>(enumVal);
|
||||||
|
|
||||||
|
if (file.read(&enumVal, 1) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.fontStyle = static_cast<CssFontStyle>(enumVal);
|
||||||
|
|
||||||
|
if (file.read(&enumVal, 1) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.fontWeight = static_cast<CssFontWeight>(enumVal);
|
||||||
|
|
||||||
|
if (file.read(&enumVal, 1) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.textDecoration = static_cast<CssTextDecoration>(enumVal);
|
||||||
|
|
||||||
|
auto readLength = [&file](CssLength& len) -> bool {
|
||||||
|
if (file.read(&len.value, sizeof(len.value)) != sizeof(len.value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint8_t unitVal;
|
||||||
|
if (file.read(&unitVal, 1) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
len.unit = static_cast<CssUnit>(unitVal);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) ||
|
||||||
|
!readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) ||
|
||||||
|
!readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight) ||
|
||||||
|
!readLength(style.imageHeight) || !readLength(style.imageWidth)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t displayVal;
|
||||||
|
if (file.read(&displayVal, 1) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.display = static_cast<CssDisplay>(displayVal);
|
||||||
|
|
||||||
|
uint16_t definedBits = 0;
|
||||||
|
if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.defined.textAlign = (definedBits & 1 << 0) != 0;
|
||||||
|
style.defined.fontStyle = (definedBits & 1 << 1) != 0;
|
||||||
|
style.defined.fontWeight = (definedBits & 1 << 2) != 0;
|
||||||
|
style.defined.textDecoration = (definedBits & 1 << 3) != 0;
|
||||||
|
style.defined.textIndent = (definedBits & 1 << 4) != 0;
|
||||||
|
style.defined.marginTop = (definedBits & 1 << 5) != 0;
|
||||||
|
style.defined.marginBottom = (definedBits & 1 << 6) != 0;
|
||||||
|
style.defined.marginLeft = (definedBits & 1 << 7) != 0;
|
||||||
|
style.defined.marginRight = (definedBits & 1 << 8) != 0;
|
||||||
|
style.defined.paddingTop = (definedBits & 1 << 9) != 0;
|
||||||
|
style.defined.paddingBottom = (definedBits & 1 << 10) != 0;
|
||||||
|
style.defined.paddingLeft = (definedBits & 1 << 11) != 0;
|
||||||
|
style.defined.paddingRight = (definedBits & 1 << 12) != 0;
|
||||||
|
style.defined.imageHeight = (definedBits & 1 << 13) != 0;
|
||||||
|
style.defined.imageWidth = (definedBits & 1 << 14) != 0;
|
||||||
|
style.defined.display = (definedBits & 1 << 15) != 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CssParser::writeCssStylePayload(FsFile& file, const CssStyle& style) {
|
||||||
|
file.write(static_cast<uint8_t>(style.textAlign));
|
||||||
|
file.write(static_cast<uint8_t>(style.fontStyle));
|
||||||
|
file.write(static_cast<uint8_t>(style.fontWeight));
|
||||||
|
file.write(static_cast<uint8_t>(style.textDecoration));
|
||||||
|
|
||||||
|
auto writeLength = [&file](const CssLength& len) {
|
||||||
|
file.write(reinterpret_cast<const uint8_t*>(&len.value), sizeof(len.value));
|
||||||
|
file.write(static_cast<uint8_t>(len.unit));
|
||||||
|
};
|
||||||
|
|
||||||
|
writeLength(style.textIndent);
|
||||||
|
writeLength(style.marginTop);
|
||||||
|
writeLength(style.marginBottom);
|
||||||
|
writeLength(style.marginLeft);
|
||||||
|
writeLength(style.marginRight);
|
||||||
|
writeLength(style.paddingTop);
|
||||||
|
writeLength(style.paddingBottom);
|
||||||
|
writeLength(style.paddingLeft);
|
||||||
|
writeLength(style.paddingRight);
|
||||||
|
writeLength(style.imageHeight);
|
||||||
|
writeLength(style.imageWidth);
|
||||||
|
file.write(static_cast<uint8_t>(style.display));
|
||||||
|
|
||||||
|
uint16_t definedBits = 0;
|
||||||
|
if (style.defined.textAlign) definedBits |= 1 << 0;
|
||||||
|
if (style.defined.fontStyle) definedBits |= 1 << 1;
|
||||||
|
if (style.defined.fontWeight) definedBits |= 1 << 2;
|
||||||
|
if (style.defined.textDecoration) definedBits |= 1 << 3;
|
||||||
|
if (style.defined.textIndent) definedBits |= 1 << 4;
|
||||||
|
if (style.defined.marginTop) definedBits |= 1 << 5;
|
||||||
|
if (style.defined.marginBottom) definedBits |= 1 << 6;
|
||||||
|
if (style.defined.marginLeft) definedBits |= 1 << 7;
|
||||||
|
if (style.defined.marginRight) definedBits |= 1 << 8;
|
||||||
|
if (style.defined.paddingTop) definedBits |= 1 << 9;
|
||||||
|
if (style.defined.paddingBottom) definedBits |= 1 << 10;
|
||||||
|
if (style.defined.paddingLeft) definedBits |= 1 << 11;
|
||||||
|
if (style.defined.paddingRight) definedBits |= 1 << 12;
|
||||||
|
if (style.defined.imageHeight) definedBits |= 1 << 13;
|
||||||
|
if (style.defined.imageWidth) definedBits |= 1 << 14;
|
||||||
|
if (style.defined.display) definedBits |= 1 << 15;
|
||||||
|
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CssParser::touchHotRule(const std::string& selector) const {
|
||||||
|
auto it = hotRuleCache_.find(selector);
|
||||||
|
if (it == hotRuleCache_.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hotRuleLru_.erase(it->second.second);
|
||||||
|
hotRuleLru_.push_front(selector);
|
||||||
|
it->second.second = hotRuleLru_.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CssParser::cacheHotRule(const std::string& selector, const CssStyle& style) const {
|
||||||
|
auto it = hotRuleCache_.find(selector);
|
||||||
|
if (it != hotRuleCache_.end()) {
|
||||||
|
it->second.first = style;
|
||||||
|
touchHotRule(selector);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hotRuleLru_.push_front(selector);
|
||||||
|
hotRuleCache_.emplace(selector, std::make_pair(style, hotRuleLru_.begin()));
|
||||||
|
if (hotRuleCache_.size() > HOT_RULE_CACHE_SIZE) {
|
||||||
|
const std::string& evictKey = hotRuleLru_.back();
|
||||||
|
hotRuleCache_.erase(evictKey);
|
||||||
|
hotRuleLru_.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::readRuleFromDiskAtOffset(const uint32_t styleOffset, CssStyle& outStyle) const {
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForRead("CSS", cachePath + rulesCache, file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!file.seek(styleOffset)) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bool ok = readCssStylePayload(file, outStyle);
|
||||||
|
file.close();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::lookupRule(const std::string& selector, CssStyle& outStyle) const {
|
||||||
|
auto mapIt = rulesBySelector_.find(selector);
|
||||||
|
if (mapIt != rulesBySelector_.end()) {
|
||||||
|
outStyle = mapIt->second;
|
||||||
|
resolveStats_.mapHits++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto hotIt = hotRuleCache_.find(selector);
|
||||||
|
if (hotIt != hotRuleCache_.end()) {
|
||||||
|
outStyle = hotIt->second.first;
|
||||||
|
touchHotRule(selector);
|
||||||
|
resolveStats_.hotHits++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (negativeRuleCache_.find(selector) != negativeRuleCache_.end()) {
|
||||||
|
resolveStats_.negativeHits++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ensureCacheIndexLoaded()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto offsetIt = cacheRuleOffsets_.find(selector);
|
||||||
|
if (offsetIt == cacheRuleOffsets_.end()) {
|
||||||
|
if (negativeRuleCache_.size() >= NEGATIVE_CACHE_SIZE) {
|
||||||
|
negativeRuleCache_.clear();
|
||||||
|
}
|
||||||
|
negativeRuleCache_.insert(selector);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!readRuleFromDiskAtOffset(offsetIt->second, outStyle)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheHotRule(selector, outStyle);
|
||||||
|
resolveStats_.diskHits++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::ensureCacheIndexLoaded() const {
|
||||||
|
if (cacheIndexLoaded_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForRead("CSS", cachePath + rulesCache, file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t version = 0;
|
||||||
|
if (file.read(&version, 1) != 1 || version != CssParser::CSS_CACHE_VERSION) {
|
||||||
|
file.close();
|
||||||
|
Storage.remove((cachePath + rulesCache).c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t ruleCount = 0;
|
||||||
|
if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount) || ruleCount > MAX_RULES) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheRuleOffsets_.clear();
|
||||||
|
cacheRuleOffsets_.reserve(ruleCount);
|
||||||
|
hotRuleCache_.clear();
|
||||||
|
hotRuleLru_.clear();
|
||||||
|
negativeRuleCache_.clear();
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < ruleCount; ++i) {
|
||||||
|
uint16_t selectorLen = 0;
|
||||||
|
if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen) || selectorLen == 0 ||
|
||||||
|
selectorLen > MAX_SELECTOR_LENGTH) {
|
||||||
|
file.close();
|
||||||
|
cacheRuleOffsets_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string selector;
|
||||||
|
selector.resize(selectorLen);
|
||||||
|
if (file.read(&selector[0], selectorLen) != selectorLen) {
|
||||||
|
file.close();
|
||||||
|
cacheRuleOffsets_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t styleOffset = file.position();
|
||||||
|
cacheRuleOffsets_[std::move(selector)] = styleOffset;
|
||||||
|
if (!file.seek(styleOffset + CSS_FIXED_STYLE_BYTES)) {
|
||||||
|
file.close();
|
||||||
|
cacheRuleOffsets_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedRuleCount_ = cacheRuleOffsets_.size();
|
||||||
|
cacheIndexLoaded_ = true;
|
||||||
|
file.close();
|
||||||
|
LOG_DBG("CSS", "Loaded CSS index: %u selectors (hot cache size=%u)", static_cast<unsigned>(cachedRuleCount_),
|
||||||
|
static_cast<unsigned>(HOT_RULE_CACHE_SIZE));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Style resolution
|
// Style resolution
|
||||||
|
|
||||||
CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& classAttr) const {
|
CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& classAttr) const {
|
||||||
static bool lowHeapWarningLogged = false;
|
static bool lowHeapWarningLogged = false;
|
||||||
|
resolveStats_.resolveCalls++;
|
||||||
if (ESP.getFreeHeap() < MIN_FREE_HEAP_FOR_CSS) {
|
if (ESP.getFreeHeap() < MIN_FREE_HEAP_FOR_CSS) {
|
||||||
if (!lowHeapWarningLogged) {
|
if (!lowHeapWarningLogged) {
|
||||||
lowHeapWarningLogged = true;
|
lowHeapWarningLogged = true;
|
||||||
LOG_DBG("CSS", "Warning: low heap (%u bytes) below MIN_FREE_HEAP_FOR_CSS (%u), returning empty style",
|
LOG_DBG("CSS", "Warning: low heap (%u bytes) below MIN_FREE_HEAP_FOR_CSS (%u), returning empty style",
|
||||||
ESP.getFreeHeap(), static_cast<unsigned>(MIN_FREE_HEAP_FOR_CSS));
|
ESP.getFreeHeap(), static_cast<unsigned>(MIN_FREE_HEAP_FOR_CSS));
|
||||||
}
|
}
|
||||||
|
resolveStats_.lowHeapSkips++;
|
||||||
return CssStyle{};
|
return CssStyle{};
|
||||||
}
|
}
|
||||||
CssStyle result;
|
CssStyle result;
|
||||||
const std::string tag = normalized(tagName);
|
const std::string tag = normalized(tagName);
|
||||||
|
|
||||||
// 1. Apply element-level style (lowest priority)
|
// 1. Apply element-level style (lowest priority)
|
||||||
const auto tagIt = rulesBySelector_.find(tag);
|
{
|
||||||
if (tagIt != rulesBySelector_.end()) {
|
CssStyle tagStyle;
|
||||||
result.applyOver(tagIt->second);
|
if (lookupRule(tag, tagStyle)) {
|
||||||
|
result.applyOver(tagStyle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Support combinations of classes (e.g. style on .class1.class2)
|
// TODO: Support combinations of classes (e.g. style on .class1.class2)
|
||||||
@@ -637,9 +1094,9 @@ CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string&
|
|||||||
for (const auto& cls : classes) {
|
for (const auto& cls : classes) {
|
||||||
std::string classKey = "." + normalized(cls);
|
std::string classKey = "." + normalized(cls);
|
||||||
|
|
||||||
auto classIt = rulesBySelector_.find(classKey);
|
CssStyle classStyle;
|
||||||
if (classIt != rulesBySelector_.end()) {
|
if (lookupRule(classKey, classStyle)) {
|
||||||
result.applyOver(classIt->second);
|
result.applyOver(classStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,13 +1105,17 @@ CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string&
|
|||||||
for (const auto& cls : classes) {
|
for (const auto& cls : classes) {
|
||||||
std::string combinedKey = tag + "." + normalized(cls);
|
std::string combinedKey = tag + "." + normalized(cls);
|
||||||
|
|
||||||
auto combinedIt = rulesBySelector_.find(combinedKey);
|
CssStyle combinedStyle;
|
||||||
if (combinedIt != rulesBySelector_.end()) {
|
if (lookupRule(combinedKey, combinedStyle)) {
|
||||||
result.applyOver(combinedIt->second);
|
result.applyOver(combinedStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!result.defined.anySet()) {
|
||||||
|
resolveStats_.misses++;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,9 +1125,6 @@ CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return par
|
|||||||
|
|
||||||
// Cache serialization
|
// Cache serialization
|
||||||
|
|
||||||
// Cache file name (version is CssParser::CSS_CACHE_VERSION)
|
|
||||||
constexpr char rulesCache[] = "/css_rules.cache";
|
|
||||||
|
|
||||||
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
|
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
|
||||||
|
|
||||||
void CssParser::deleteCache() const {
|
void CssParser::deleteCache() const {
|
||||||
@@ -754,175 +1212,19 @@ bool CssParser::loadFromCache() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile file;
|
// Drop parse-time in-memory rules, then initialize on-disk selector index.
|
||||||
if (!Storage.openFileForRead("CSS", cachePath + rulesCache, file)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear existing rules
|
|
||||||
clear();
|
|
||||||
|
|
||||||
// Read and verify version
|
|
||||||
uint8_t version = 0;
|
|
||||||
if (file.read(&version, 1) != 1 || version != CssParser::CSS_CACHE_VERSION) {
|
|
||||||
LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u), removing stale cache for rebuild", version,
|
|
||||||
CssParser::CSS_CACHE_VERSION);
|
|
||||||
file.close();
|
|
||||||
Storage.remove((cachePath + rulesCache).c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read rule count
|
|
||||||
uint16_t ruleCount = 0;
|
|
||||||
if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount)) {
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ruleCount > MAX_RULES) {
|
|
||||||
LOG_DBG("CSS", "Invalid cache rule count (%u > %zu)", ruleCount, MAX_RULES);
|
|
||||||
rulesBySelector_.clear();
|
rulesBySelector_.clear();
|
||||||
file.close();
|
hotRuleCache_.clear();
|
||||||
|
hotRuleLru_.clear();
|
||||||
|
negativeRuleCache_.clear();
|
||||||
|
cacheRuleOffsets_.clear();
|
||||||
|
cacheIndexLoaded_ = false;
|
||||||
|
cachedRuleCount_ = 0;
|
||||||
|
|
||||||
|
if (!ensureCacheIndexLoaded()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool {
|
LOG_DBG("CSS", "Loaded %u rules from cache index", static_cast<unsigned>(cachedRuleCount_));
|
||||||
return static_cast<size_t>(file.available()) >= neededBytes;
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr size_t CSS_LENGTH_FIELD_COUNT = 11;
|
|
||||||
constexpr size_t CSS_LENGTH_BYTES = sizeof(float) + sizeof(uint8_t);
|
|
||||||
constexpr size_t CSS_FIXED_STYLE_BYTES =
|
|
||||||
4 * sizeof(uint8_t) + (CSS_LENGTH_FIELD_COUNT * CSS_LENGTH_BYTES) + sizeof(uint8_t) + sizeof(uint16_t);
|
|
||||||
|
|
||||||
// Read each rule
|
|
||||||
for (uint16_t i = 0; i < ruleCount; ++i) {
|
|
||||||
// Read selector string
|
|
||||||
uint16_t selectorLen = 0;
|
|
||||||
if (!hasRemainingBytes(sizeof(selectorLen))) {
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) {
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectorLen == 0 || selectorLen > MAX_SELECTOR_LENGTH || !hasRemainingBytes(selectorLen)) {
|
|
||||||
LOG_DBG("CSS", "Invalid selector length in cache: %u", selectorLen);
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string selector;
|
|
||||||
selector.resize(selectorLen);
|
|
||||||
if (file.read(&selector[0], selectorLen) != selectorLen) {
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasRemainingBytes(CSS_FIXED_STYLE_BYTES)) {
|
|
||||||
LOG_DBG("CSS", "Truncated CSS cache while reading style payload");
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read CssStyle fields
|
|
||||||
CssStyle style;
|
|
||||||
uint8_t enumVal;
|
|
||||||
|
|
||||||
if (file.read(&enumVal, 1) != 1) {
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
style.textAlign = static_cast<CssTextAlign>(enumVal);
|
|
||||||
|
|
||||||
if (file.read(&enumVal, 1) != 1) {
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
style.fontStyle = static_cast<CssFontStyle>(enumVal);
|
|
||||||
|
|
||||||
if (file.read(&enumVal, 1) != 1) {
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
style.fontWeight = static_cast<CssFontWeight>(enumVal);
|
|
||||||
|
|
||||||
if (file.read(&enumVal, 1) != 1) {
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
style.textDecoration = static_cast<CssTextDecoration>(enumVal);
|
|
||||||
|
|
||||||
// Read CssLength fields
|
|
||||||
auto readLength = [&file](CssLength& len) -> bool {
|
|
||||||
if (file.read(&len.value, sizeof(len.value)) != sizeof(len.value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
uint8_t unitVal;
|
|
||||||
if (file.read(&unitVal, 1) != 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
len.unit = static_cast<CssUnit>(unitVal);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) ||
|
|
||||||
!readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) ||
|
|
||||||
!readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight) ||
|
|
||||||
!readLength(style.imageHeight) || !readLength(style.imageWidth)) {
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read display value
|
|
||||||
uint8_t displayVal;
|
|
||||||
if (file.read(&displayVal, 1) != 1) {
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
style.display = static_cast<CssDisplay>(displayVal);
|
|
||||||
|
|
||||||
// Read defined flags
|
|
||||||
uint16_t definedBits = 0;
|
|
||||||
if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) {
|
|
||||||
rulesBySelector_.clear();
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
style.defined.textAlign = (definedBits & 1 << 0) != 0;
|
|
||||||
style.defined.fontStyle = (definedBits & 1 << 1) != 0;
|
|
||||||
style.defined.fontWeight = (definedBits & 1 << 2) != 0;
|
|
||||||
style.defined.textDecoration = (definedBits & 1 << 3) != 0;
|
|
||||||
style.defined.textIndent = (definedBits & 1 << 4) != 0;
|
|
||||||
style.defined.marginTop = (definedBits & 1 << 5) != 0;
|
|
||||||
style.defined.marginBottom = (definedBits & 1 << 6) != 0;
|
|
||||||
style.defined.marginLeft = (definedBits & 1 << 7) != 0;
|
|
||||||
style.defined.marginRight = (definedBits & 1 << 8) != 0;
|
|
||||||
style.defined.paddingTop = (definedBits & 1 << 9) != 0;
|
|
||||||
style.defined.paddingBottom = (definedBits & 1 << 10) != 0;
|
|
||||||
style.defined.paddingLeft = (definedBits & 1 << 11) != 0;
|
|
||||||
style.defined.paddingRight = (definedBits & 1 << 12) != 0;
|
|
||||||
style.defined.imageHeight = (definedBits & 1 << 13) != 0;
|
|
||||||
style.defined.imageWidth = (definedBits & 1 << 14) != 0;
|
|
||||||
style.defined.display = (definedBits & 1 << 15) != 0;
|
|
||||||
|
|
||||||
rulesBySelector_[selector] = style;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DBG("CSS", "Loaded %u rules from cache", ruleCount);
|
|
||||||
file.close();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
|
#include <list>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -30,6 +32,16 @@
|
|||||||
*/
|
*/
|
||||||
class CssParser {
|
class CssParser {
|
||||||
public:
|
public:
|
||||||
|
struct ResolveStats {
|
||||||
|
uint32_t resolveCalls = 0;
|
||||||
|
uint32_t lowHeapSkips = 0;
|
||||||
|
uint32_t mapHits = 0;
|
||||||
|
uint32_t hotHits = 0;
|
||||||
|
uint32_t diskHits = 0;
|
||||||
|
uint32_t misses = 0;
|
||||||
|
uint32_t negativeHits = 0;
|
||||||
|
};
|
||||||
|
|
||||||
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
|
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
|
||||||
static constexpr uint8_t CSS_CACHE_VERSION = 4;
|
static constexpr uint8_t CSS_CACHE_VERSION = 4;
|
||||||
|
|
||||||
@@ -68,17 +80,17 @@ class CssParser {
|
|||||||
/**
|
/**
|
||||||
* Check if any rules have been loaded
|
* Check if any rules have been loaded
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] bool empty() const { return rulesBySelector_.empty(); }
|
[[nodiscard]] bool empty() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get count of loaded rule sets
|
* Get count of loaded rule sets
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] size_t ruleCount() const { return rulesBySelector_.size(); }
|
[[nodiscard]] size_t ruleCount() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all loaded rules
|
* Clear all loaded rules
|
||||||
*/
|
*/
|
||||||
void clear() { rulesBySelector_.clear(); }
|
void clear();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if CSS rules cache file exists
|
* Check if CSS rules cache file exists
|
||||||
@@ -103,12 +115,43 @@ class CssParser {
|
|||||||
*/
|
*/
|
||||||
bool loadFromCache();
|
bool loadFromCache();
|
||||||
|
|
||||||
|
// Low-memory CSS compilation pipeline:
|
||||||
|
// - beginCacheCompile(): starts streaming compile mode
|
||||||
|
// - appendCompiledFromStream(): parses one stylesheet stream into compile staging
|
||||||
|
// - endCacheCompile(): finalizes cache file from staged records
|
||||||
|
bool beginCacheCompile();
|
||||||
|
bool appendCompiledFromStream(FsFile& source);
|
||||||
|
bool endCacheCompile();
|
||||||
|
|
||||||
|
// CSS lookup telemetry helpers for tuning memory/caching behavior on-device.
|
||||||
|
void resetResolveStats() const;
|
||||||
|
[[nodiscard]] ResolveStats getResolveStats() const;
|
||||||
|
void logResolveStats(const char* context) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Storage: maps normalized selector -> style properties
|
// Storage: maps normalized selector -> style properties
|
||||||
std::unordered_map<std::string, CssStyle> rulesBySelector_;
|
std::unordered_map<std::string, CssStyle> rulesBySelector_;
|
||||||
|
|
||||||
std::string cachePath;
|
std::string cachePath;
|
||||||
|
|
||||||
|
// Disk-backed CSS dictionary index: selector -> byte offset for serialized CssStyle payload.
|
||||||
|
// Built from cache file once, then styles are loaded on demand into hotRuleCache_.
|
||||||
|
mutable bool cacheIndexLoaded_ = false;
|
||||||
|
mutable size_t cachedRuleCount_ = 0;
|
||||||
|
mutable std::unordered_map<std::string, uint32_t> cacheRuleOffsets_;
|
||||||
|
|
||||||
|
// Bounded hot cache of most recently used rules.
|
||||||
|
mutable std::list<std::string> hotRuleLru_;
|
||||||
|
mutable std::unordered_map<std::string, std::pair<CssStyle, std::list<std::string>::iterator>> hotRuleCache_;
|
||||||
|
mutable std::unordered_set<std::string> negativeRuleCache_;
|
||||||
|
mutable ResolveStats resolveStats_;
|
||||||
|
|
||||||
|
bool compileModeActive_ = false;
|
||||||
|
bool compileModeFailed_ = false;
|
||||||
|
std::string compileTempPath_;
|
||||||
|
FsFile compileTempFile_;
|
||||||
|
std::unordered_map<std::string, uint32_t> compileSelectorOffsets_;
|
||||||
|
|
||||||
// Internal parsing helpers
|
// Internal parsing helpers
|
||||||
void processRuleBlockWithStyle(const std::string& selectorGroup, const CssStyle& style);
|
void processRuleBlockWithStyle(const std::string& selectorGroup, const CssStyle& style);
|
||||||
static CssStyle parseDeclarations(const std::string& declBlock);
|
static CssStyle parseDeclarations(const std::string& declBlock);
|
||||||
@@ -129,4 +172,13 @@ class CssParser {
|
|||||||
static void normalizedInto(const std::string& s, std::string& out);
|
static void normalizedInto(const std::string& s, std::string& out);
|
||||||
static std::vector<std::string> splitOnChar(const std::string& s, char delimiter);
|
static std::vector<std::string> splitOnChar(const std::string& s, char delimiter);
|
||||||
static std::vector<std::string> splitWhitespace(const std::string& s);
|
static std::vector<std::string> splitWhitespace(const std::string& s);
|
||||||
|
|
||||||
|
// On-demand rule loading helpers
|
||||||
|
bool ensureCacheIndexLoaded() const;
|
||||||
|
bool lookupRule(const std::string& selector, CssStyle& outStyle) const;
|
||||||
|
bool readRuleFromDiskAtOffset(uint32_t styleOffset, CssStyle& outStyle) const;
|
||||||
|
static bool readCssStylePayload(FsFile& file, CssStyle& style);
|
||||||
|
static void writeCssStylePayload(FsFile& file, const CssStyle& style);
|
||||||
|
void touchHotRule(const std::string& selector) const;
|
||||||
|
void cacheHotRule(const std::string& selector, const CssStyle& style) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ void GfxRenderer::begin() {
|
|||||||
panelHeight = display.getDisplayHeight();
|
panelHeight = display.getDisplayHeight();
|
||||||
panelWidthBytes = display.getDisplayWidthBytes();
|
panelWidthBytes = display.getDisplayWidthBytes();
|
||||||
frameBufferSize = display.getBufferSize();
|
frameBufferSize = display.getBufferSize();
|
||||||
|
bwSnapshotRowStart = 0;
|
||||||
|
bwSnapshotRowEnd = 0;
|
||||||
|
bwSnapshotSizeBytes = 0;
|
||||||
bwBufferChunkSize = BW_BUFFER_CHUNK_SIZE;
|
bwBufferChunkSize = BW_BUFFER_CHUNK_SIZE;
|
||||||
bwBufferChunks.assign((frameBufferSize + bwBufferChunkSize - 1) / bwBufferChunkSize, nullptr);
|
bwBufferChunks.assign((frameBufferSize + bwBufferChunkSize - 1) / bwBufferChunkSize, nullptr);
|
||||||
}
|
}
|
||||||
@@ -2143,9 +2146,75 @@ void GfxRenderer::freeBwBufferChunks() {
|
|||||||
* Uses chunked allocation to avoid needing 48KB of contiguous memory.
|
* Uses chunked allocation to avoid needing 48KB of contiguous memory.
|
||||||
* Returns true if buffer was stored successfully, false if allocation failed.
|
* Returns true if buffer was stored successfully, false if allocation failed.
|
||||||
*/
|
*/
|
||||||
bool GfxRenderer::storeBwBuffer() {
|
bool GfxRenderer::storeBwBuffer() { return storeBwBufferRect(0, 0, getScreenWidth(), getScreenHeight()); }
|
||||||
|
|
||||||
|
bool GfxRenderer::storeBwBufferRect(const int x, const int y, const int width, const int height) {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
LOG_ERR("GFX", "!! BW buffer store rect invalid: x=%d y=%d w=%d h=%d", x, y, width, height);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int screenWidth = getScreenWidth();
|
||||||
|
const int screenHeight = getScreenHeight();
|
||||||
|
if (screenWidth <= 0 || screenHeight <= 0 || panelWidthBytes == 0 || panelHeight == 0 || !frameBuffer) {
|
||||||
|
LOG_ERR("GFX", "!! BW buffer store unavailable (screen=%dx%d panelHeight=%u rowBytes=%u fb=%p)", screenWidth,
|
||||||
|
screenHeight, panelHeight, panelWidthBytes, frameBuffer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int clampedX0 = std::max(0, x);
|
||||||
|
const int clampedY0 = std::max(0, y);
|
||||||
|
const int clampedX1 = std::min(screenWidth - 1, x + width - 1);
|
||||||
|
const int clampedY1 = std::min(screenHeight - 1, y + height - 1);
|
||||||
|
if (clampedX0 > clampedX1 || clampedY0 > clampedY1) {
|
||||||
|
LOG_ERR("GFX", "!! BW buffer store rect outside screen: x=%d y=%d w=%d h=%d", x, y, width, height);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rowStart = 0;
|
||||||
|
int rowEnd = 0;
|
||||||
|
switch (getOrientation()) {
|
||||||
|
case LandscapeCounterClockwise:
|
||||||
|
rowStart = clampedY0;
|
||||||
|
rowEnd = clampedY1;
|
||||||
|
break;
|
||||||
|
case LandscapeClockwise:
|
||||||
|
rowStart = static_cast<int>(panelHeight) - 1 - clampedY1;
|
||||||
|
rowEnd = static_cast<int>(panelHeight) - 1 - clampedY0;
|
||||||
|
break;
|
||||||
|
case Portrait:
|
||||||
|
rowStart = static_cast<int>(panelHeight) - 1 - clampedX1;
|
||||||
|
rowEnd = static_cast<int>(panelHeight) - 1 - clampedX0;
|
||||||
|
break;
|
||||||
|
case PortraitInverted:
|
||||||
|
rowStart = clampedX0;
|
||||||
|
rowEnd = clampedX1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
rowStart = std::max(0, rowStart);
|
||||||
|
rowEnd = std::min(static_cast<int>(panelHeight) - 1, rowEnd);
|
||||||
|
if (rowStart > rowEnd) {
|
||||||
|
LOG_ERR("GFX", "!! BW buffer store row-band invalid after orientation mapping: rows=%d..%d", rowStart, rowEnd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t rows = static_cast<size_t>(rowEnd - rowStart + 1);
|
||||||
|
const size_t snapshotSizeBytes = rows * panelWidthBytes;
|
||||||
|
const size_t snapshotBaseOffset = static_cast<size_t>(rowStart) * panelWidthBytes;
|
||||||
|
if (snapshotSizeBytes == 0 || snapshotBaseOffset + snapshotSizeBytes > frameBufferSize) {
|
||||||
|
LOG_ERR("GFX", "!! BW buffer store row-band out of bounds: base=%zu size=%zu frame=%u", snapshotBaseOffset,
|
||||||
|
snapshotSizeBytes, frameBufferSize);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
freeBwBufferChunks();
|
||||||
|
bwSnapshotRowStart = static_cast<uint16_t>(rowStart);
|
||||||
|
bwSnapshotRowEnd = static_cast<uint16_t>(rowEnd);
|
||||||
|
bwSnapshotSizeBytes = snapshotSizeBytes;
|
||||||
|
|
||||||
auto attemptStore = [&](size_t chunkSize) {
|
auto attemptStore = [&](size_t chunkSize) {
|
||||||
bwBufferChunks.assign((frameBufferSize + chunkSize - 1) / chunkSize, nullptr);
|
bwBufferChunks.assign((bwSnapshotSizeBytes + chunkSize - 1) / chunkSize, nullptr);
|
||||||
for (size_t i = 0; i < bwBufferChunks.size(); i++) {
|
for (size_t i = 0; i < bwBufferChunks.size(); i++) {
|
||||||
if (bwBufferChunks[i]) {
|
if (bwBufferChunks[i]) {
|
||||||
LOG_ERR("GFX", "!! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk", i);
|
LOG_ERR("GFX", "!! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk", i);
|
||||||
@@ -2154,7 +2223,7 @@ bool GfxRenderer::storeBwBuffer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const size_t offset = i * chunkSize;
|
const size_t offset = i * chunkSize;
|
||||||
const size_t allocSize = std::min(chunkSize, static_cast<size_t>(frameBufferSize - offset));
|
const size_t allocSize = std::min(chunkSize, bwSnapshotSizeBytes - offset);
|
||||||
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(allocSize));
|
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(allocSize));
|
||||||
|
|
||||||
if (!bwBufferChunks[i]) {
|
if (!bwBufferChunks[i]) {
|
||||||
@@ -2166,10 +2235,11 @@ bool GfxRenderer::storeBwBuffer() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(bwBufferChunks[i], frameBuffer + offset, allocSize);
|
memcpy(bwBufferChunks[i], frameBuffer + snapshotBaseOffset + offset, allocSize);
|
||||||
}
|
}
|
||||||
bwBufferChunkSize = chunkSize;
|
bwBufferChunkSize = chunkSize;
|
||||||
LOG_DBG("GFX", "Stored BW buffer in %zu chunks (%zu bytes each)", bwBufferChunks.size(), chunkSize);
|
LOG_DBG("GFX", "Stored BW buffer rows [%u..%u] (%zu bytes) in %zu chunks (%zu bytes each)", bwSnapshotRowStart,
|
||||||
|
bwSnapshotRowEnd, bwSnapshotSizeBytes, bwBufferChunks.size(), chunkSize);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2199,6 +2269,9 @@ bool GfxRenderer::storeBwBuffer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOG_ERR("GFX", "!! BW buffer storage failed after retrying smaller chunk sizes");
|
LOG_ERR("GFX", "!! BW buffer storage failed after retrying smaller chunk sizes");
|
||||||
|
bwSnapshotSizeBytes = 0;
|
||||||
|
bwSnapshotRowStart = 0;
|
||||||
|
bwSnapshotRowEnd = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2208,6 +2281,13 @@ bool GfxRenderer::storeBwBuffer() {
|
|||||||
* Uses chunked restoration to match chunked storage.
|
* Uses chunked restoration to match chunked storage.
|
||||||
*/
|
*/
|
||||||
void GfxRenderer::restoreBwBuffer() {
|
void GfxRenderer::restoreBwBuffer() {
|
||||||
|
if (bwSnapshotSizeBytes == 0) {
|
||||||
|
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||||
|
freeBwBufferChunks();
|
||||||
|
LOG_ERR("GFX", "BW restore skipped: no stored snapshot metadata; cleaned grayscale buffers only");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if all chunks are allocated
|
// Check if all chunks are allocated
|
||||||
bool missingChunks = false;
|
bool missingChunks = false;
|
||||||
for (const auto& bwBufferChunk : bwBufferChunks) {
|
for (const auto& bwBufferChunk : bwBufferChunks) {
|
||||||
@@ -2223,20 +2303,28 @@ void GfxRenderer::restoreBwBuffer() {
|
|||||||
// allocations that can later starve TLS handshakes.
|
// allocations that can later starve TLS handshakes.
|
||||||
display.cleanupGrayscaleBuffers(frameBuffer);
|
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||||
freeBwBufferChunks();
|
freeBwBufferChunks();
|
||||||
|
bwSnapshotSizeBytes = 0;
|
||||||
|
bwSnapshotRowStart = 0;
|
||||||
|
bwSnapshotRowEnd = 0;
|
||||||
LOG_ERR("GFX", "BW restore skipped due to missing chunks; cleaned grayscale buffers only");
|
LOG_ERR("GFX", "BW restore skipped due to missing chunks; cleaned grayscale buffers only");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const size_t snapshotBaseOffset = static_cast<size_t>(bwSnapshotRowStart) * panelWidthBytes;
|
||||||
for (size_t i = 0; i < bwBufferChunks.size(); i++) {
|
for (size_t i = 0; i < bwBufferChunks.size(); i++) {
|
||||||
const size_t offset = i * bwBufferChunkSize;
|
const size_t offset = i * bwBufferChunkSize;
|
||||||
const size_t chunkSize = std::min(bwBufferChunkSize, static_cast<size_t>(frameBufferSize - offset));
|
const size_t chunkSize = std::min(bwBufferChunkSize, bwSnapshotSizeBytes - offset);
|
||||||
memcpy(frameBuffer + offset, bwBufferChunks[i], chunkSize);
|
memcpy(frameBuffer + snapshotBaseOffset + offset, bwBufferChunks[i], chunkSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
display.cleanupGrayscaleBuffers(frameBuffer);
|
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||||
|
|
||||||
freeBwBufferChunks();
|
freeBwBufferChunks();
|
||||||
LOG_DBG("GFX", "Restored and freed BW buffer chunks");
|
LOG_DBG("GFX", "Restored BW buffer rows [%u..%u] (%zu bytes) and freed BW chunks", bwSnapshotRowStart,
|
||||||
|
bwSnapshotRowEnd, bwSnapshotSizeBytes);
|
||||||
|
bwSnapshotSizeBytes = 0;
|
||||||
|
bwSnapshotRowStart = 0;
|
||||||
|
bwSnapshotRowEnd = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ class GfxRenderer {
|
|||||||
uint16_t panelHeight = 0; // set in begin()
|
uint16_t panelHeight = 0; // set in begin()
|
||||||
uint16_t panelWidthBytes = 0; // set in begin()
|
uint16_t panelWidthBytes = 0; // set in begin()
|
||||||
uint32_t frameBufferSize = 0; // set in begin()
|
uint32_t frameBufferSize = 0; // set in begin()
|
||||||
|
uint16_t bwSnapshotRowStart = 0;
|
||||||
|
uint16_t bwSnapshotRowEnd = 0;
|
||||||
|
size_t bwSnapshotSizeBytes = 0;
|
||||||
size_t bwBufferChunkSize = BW_BUFFER_CHUNK_SIZE;
|
size_t bwBufferChunkSize = BW_BUFFER_CHUNK_SIZE;
|
||||||
std::vector<uint8_t*> bwBufferChunks;
|
std::vector<uint8_t*> bwBufferChunks;
|
||||||
std::map<int, EpdFontFamily> fontMap;
|
std::map<int, EpdFontFamily> fontMap;
|
||||||
@@ -207,6 +210,7 @@ class GfxRenderer {
|
|||||||
void copyGrayscaleMsbBuffers() const;
|
void copyGrayscaleMsbBuffers() const;
|
||||||
void displayGrayBuffer() const;
|
void displayGrayBuffer() const;
|
||||||
bool storeBwBuffer(); // Returns true if buffer was stored successfully
|
bool storeBwBuffer(); // Returns true if buffer was stored successfully
|
||||||
|
bool storeBwBufferRect(int x, int y, int width, int height); // Store only rows intersecting logical rect
|
||||||
void restoreBwBuffer(); // Restore and free the stored buffer
|
void restoreBwBuffer(); // Restore and free the stored buffer
|
||||||
void cleanupGrayscaleWithFrameBuffer() const;
|
void cleanupGrayscaleWithFrameBuffer() const;
|
||||||
|
|
||||||
|
|||||||
@@ -75,11 +75,11 @@ constexpr uint32_t AA_RECOVERY_CONTIG_HEAP_BYTES = CP_AA_RECOVERY_CONTIG_HEAP_BY
|
|||||||
// Snapshotting BW buffer for grayscale restore can fragment heap heavily on tight
|
// Snapshotting BW buffer for grayscale restore can fragment heap heavily on tight
|
||||||
// pages. Skip attempting snapshot altogether below this safety window.
|
// pages. Skip attempting snapshot altogether below this safety window.
|
||||||
#ifndef CP_BW_SNAPSHOT_MIN_FREE_HEAP_BYTES
|
#ifndef CP_BW_SNAPSHOT_MIN_FREE_HEAP_BYTES
|
||||||
#define CP_BW_SNAPSHOT_MIN_FREE_HEAP_BYTES (72 * 1024)
|
#define CP_BW_SNAPSHOT_MIN_FREE_HEAP_BYTES (64 * 1024)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef CP_BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES
|
#ifndef CP_BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES
|
||||||
#define CP_BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES (52 * 1024)
|
#define CP_BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES (24 * 1024)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
constexpr uint32_t BW_SNAPSHOT_MIN_FREE_HEAP_BYTES = CP_BW_SNAPSHOT_MIN_FREE_HEAP_BYTES;
|
constexpr uint32_t BW_SNAPSHOT_MIN_FREE_HEAP_BYTES = CP_BW_SNAPSHOT_MIN_FREE_HEAP_BYTES;
|
||||||
@@ -98,6 +98,62 @@ void logReaderMemSnapshot(const char* stage) {
|
|||||||
inline void logReaderMemSnapshot(const char*) {}
|
inline void logReaderMemSnapshot(const char*) {}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
bool computePageDynamicYBand(const Page& page, const GfxRenderer& renderer, const int fontId, const int viewportHeight,
|
||||||
|
int* outTop, int* outBottom) {
|
||||||
|
if (viewportHeight <= 0 || !outTop || !outBottom) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasRange = false;
|
||||||
|
int minY = viewportHeight;
|
||||||
|
int maxY = -1;
|
||||||
|
const int lineHeight = std::max(1, renderer.getLineHeight(fontId));
|
||||||
|
|
||||||
|
for (const auto& el : page.elements) {
|
||||||
|
if (!el) continue;
|
||||||
|
|
||||||
|
int elementTop = el->yPos;
|
||||||
|
int elementBottom = el->yPos;
|
||||||
|
switch (el->getTag()) {
|
||||||
|
case TAG_PageLine:
|
||||||
|
elementBottom = el->yPos + lineHeight;
|
||||||
|
break;
|
||||||
|
case TAG_PageImage: {
|
||||||
|
const auto& img = static_cast<const PageImage&>(*el);
|
||||||
|
elementBottom = el->yPos + img.getImageBlock().getHeight();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TAG_PageTable: {
|
||||||
|
const auto& table = static_cast<const PageTableFragment&>(*el);
|
||||||
|
elementBottom = el->yPos + table.getTotalHeight();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
minY = std::min(minY, elementTop);
|
||||||
|
maxY = std::max(maxY, elementBottom);
|
||||||
|
hasRange = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRange) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int BAND_PAD_PX = 2;
|
||||||
|
minY = std::max(0, minY - BAND_PAD_PX);
|
||||||
|
maxY = std::min(viewportHeight, maxY + BAND_PAD_PX);
|
||||||
|
|
||||||
|
if (minY >= maxY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*outTop = minY;
|
||||||
|
*outBottom = maxY;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Computes the [0..100] EPUB progress percent. Returns 0 when pageCount is unknown (sync/bookmark
|
// Computes the [0..100] EPUB progress percent. Returns 0 when pageCount is unknown (sync/bookmark
|
||||||
// pre-render writes), in which case the next saveProgress() will overwrite progress.bin with the
|
// pre-render writes), in which case the next saveProgress() will overwrite progress.bin with the
|
||||||
// real value before the user can leave the reader.
|
// real value before the user can leave the reader.
|
||||||
@@ -1708,7 +1764,20 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
logReaderMemSnapshot("bw_store_begin");
|
logReaderMemSnapshot("bw_store_begin");
|
||||||
bool bwBufferStored = false;
|
bool bwBufferStored = false;
|
||||||
if (shouldAttemptBwSnapshot) {
|
if (shouldAttemptBwSnapshot) {
|
||||||
bwBufferStored = renderer.storeBwBuffer();
|
const int contentLeft = orientedMarginLeft;
|
||||||
|
const int contentRight = std::max(contentLeft, renderer.getScreenWidth() - orientedMarginRight);
|
||||||
|
const int contentBottom = std::max(contentTop, renderer.getScreenHeight() - orientedMarginBottom);
|
||||||
|
|
||||||
|
int bandTop = 0;
|
||||||
|
int bandBottom = std::max(0, contentBottom - contentTop);
|
||||||
|
if (!computePageDynamicYBand(*page, renderer, getEffectiveReaderFontId(), bandBottom, &bandTop, &bandBottom)) {
|
||||||
|
bandTop = 0;
|
||||||
|
bandBottom = std::max(0, contentBottom - contentTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int snapshotTop = contentTop + bandTop;
|
||||||
|
const int snapshotHeight = std::max(0, bandBottom - bandTop);
|
||||||
|
bwBufferStored = renderer.storeBwBufferRect(contentLeft, snapshotTop, contentRight - contentLeft, snapshotHeight);
|
||||||
} else {
|
} else {
|
||||||
LOG_INF("ERS", "Skipping BW snapshot precheck (free=%lu contig=%lu, need free>=%lu contig>=%lu)", bwStoreFreeHeap,
|
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),
|
bwStoreContigHeap, static_cast<uint32_t>(BW_SNAPSHOT_MIN_FREE_HEAP_BYTES),
|
||||||
|
|||||||
Reference in New Issue
Block a user