Compare commits

..
Author SHA1 Message Date
Justin Mitchell 27c4be8a34 Implement chunked allocation for glyph bitmaps
Replace single contiguous allocation with fixed-size chunks to handle heap fragmentation. Glyph bitmaps are now stored in 4KB chunks (up to 24 chunks per style/page) to allow allocation when memory is fragmented. Each glyph bitmap fits entirely within one chunk and is addressed via virtual offset.
2026-08-05 18:42:44 -04:00
103 changed files with 717 additions and 4161 deletions

No files matched your search

+2 -1
View File
@@ -29,5 +29,6 @@ lib/EpdFont/scripts/output/
/.dummy
/CMakeLists.txt
/dependencies.lock
/sdkconfig.*
/sdkconfig.default
/sdkconfig.defaults
sdkconfig.sticky
-1
View File
@@ -153,7 +153,6 @@ To convert your own TrueType/OpenType fonts:
| `armenian` | Armenian |
| `ethiopic` | Ethiopic + Extended |
| `vietnamese` | Vietnamese subset (ơ/ư and combining marks) |
| `ipa-chars` | IPA Extensions + Spacing Modifier Letters (phonetic transcription) |
| `punctuation` | General punctuation (U+2000–U+206F) |
| `cjk` | CJK Unified Ideographs + Hiragana + Katakana + Fullwidth |
| `hangul` | Korean Hangul syllables + Jamo + Compatibility Jamo |
-5
View File
@@ -369,11 +369,6 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
}
stats.pageBufferBytes += totalBytes;
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
// MEMFIX-PORT: page-slot address landmark for the heap map; portable
// Landmark for the heap block map: page slots are the largest flash-font
// allocations and otherwise show up as anonymous ~4-20 KB used blocks.
LOG_DBG("FDC", "page slot buffer=%p bytes=%u glyphs=%u", static_cast<void*>(slot.buffer), (unsigned)totalBytes,
(unsigned)glyphCount);
slot.fontData = fontData;
slot.glyphCount = glyphCount;
+26 -83
View File
@@ -191,10 +191,6 @@ void SdCardFont::freeStyleAll(PerStyle& s) {
void SdCardFont::freeAll() {
clearOverflow();
clearPersistentCache();
delete[] overflowEmergencyBuf_;
overflowEmergencyBuf_ = nullptr;
overflowEmergencyCap_ = 0;
overflowEmergency_ = {};
for (uint8_t i = 0; i < MAX_STYLES; i++) {
freeStyleAll(styles_[i]);
}
@@ -711,12 +707,6 @@ bool SdCardFont::load(const char* path) {
applyGlyphMissCallback(i);
}
// Reserve the overflow fallback buffer now, while the heap is at its healthiest
// (font load runs before the per-page render allocations). Best-effort: if it
// can't allocate, onGlyphMiss() simply keeps its per-glyph malloc path.
overflowEmergencyBuf_ = new (std::nothrow) uint8_t[OVERFLOW_EMERGENCY_BYTES];
overflowEmergencyCap_ = overflowEmergencyBuf_ ? OVERFLOW_EMERGENCY_BYTES : 0;
loaded_ = true;
LOG_DBG("SDCF", "Loaded: %s (v%u, %u styles)", path, CPFONT_VERSION, styleCount_);
@@ -1023,21 +1013,20 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
totalBitmapSize += s.miniGlyphs[i].dataLength;
}
// Read bitmap data sorted by file offset (sequential glyphs then avoid a seek).
// Read bitmap data sorted by file offset
std::sort(readOrder, readOrder + validCount,
[&](uint32_t a, uint32_t b) { return s.miniGlyphs[a].dataOffset < s.miniGlyphs[b].dataOffset; });
// Place each glyph wholly within one fixed-size chunk (padding to the next chunk
// when it wouldn't fit the remainder), so a large page's bitmap fits a fragmented
// heap yet the render still gets a contiguous per-glyph pointer. `span` is the
// byte offset across the logical chunk sequence and becomes glyph->dataOffset;
// chunks are allocated on demand and kept across pages. Any per-chunk alloc
// failure drops to the stub (overflow path), exactly as the old single-buffer
// allocation did.
// Assemble the bitmap arena from on-demand 4 KB chunks (see MINI_BM_CHUNK_*).
// `span` is the running virtual offset into the arena; each glyph is placed
// wholly within one chunk, skipping past a boundary rather than straddling it.
// glyph.dataOffset becomes the virtual offset, decoded at render time by
// miniGlyphBitmap(). Chunks are allocated only as span reaches them, so a
// sparse page never allocates the full ceiling.
uint32_t span = 0;
uint32_t lastBitmapEnd = UINT32_MAX;
for (uint32_t i = 0; i < validCount; i++) {
const uint32_t mapIdx = readOrder[i];
uint32_t mapIdx = readOrder[i];
EpdGlyph& glyph = s.miniGlyphs[mapIdx];
const uint32_t len = glyph.dataLength;
@@ -1046,16 +1035,17 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
continue;
}
if (len > MINI_BM_CHUNK_SIZE) {
// A glyph bigger than one chunk can't be placed contiguously. Reading fonts
// never hit this; bail to the stub rather than corrupt the layout.
// A single glyph larger than one chunk cannot be placed. Real fonts never
// hit this (a 4 KB 2bpp glyph would be enormous); bail rather than corrupt.
LOG_ERR("SDCF", "Prewarm: glyph %u B exceeds chunk %u B (style %u)", len, MINI_BM_CHUNK_SIZE, styleIdx);
file.close();
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
// Avoid straddling a chunk boundary.
// Skip to the next chunk boundary if this glyph would straddle one.
const uint32_t within = span & (MINI_BM_CHUNK_SIZE - 1);
if (within != 0 && within + len > MINI_BM_CHUNK_SIZE) {
span += MINI_BM_CHUNK_SIZE - within;
@@ -1063,6 +1053,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
const uint32_t chunkIdx = span >> MINI_BM_CHUNK_SHIFT;
if (chunkIdx >= MINI_BM_MAX_CHUNKS) {
LOG_ERR("SDCF", "Prewarm: mini bitmap needs > %u chunks (style %u)", MINI_BM_MAX_CHUNKS, styleIdx);
file.close();
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
@@ -1072,6 +1063,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
s.miniBitmapChunks[chunkIdx] = new (std::nothrow) uint8_t[MINI_BM_CHUNK_SIZE];
if (!s.miniBitmapChunks[chunkIdx]) {
LOG_ERR("SDCF", "Failed to allocate mini bitmap chunk %u (style %u)", chunkIdx, styleIdx);
file.close();
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
@@ -1081,7 +1073,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
}
const uint32_t off = span & (MINI_BM_CHUNK_SIZE - 1);
const uint32_t fileOff = s.bitmapFileOffset + glyph.dataOffset;
uint32_t fileOff = s.bitmapFileOffset + glyph.dataOffset;
if (fileOff != lastBitmapEnd) {
if (!file.seekSet(fileOff)) {
LOG_ERR("SDCF", "Prewarm: failed to seek to bitmap (style %u)", styleIdx);
@@ -1105,7 +1097,8 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
glyph.dataOffset = span;
span += len;
}
s.miniBitmapUsed = span; // footprint incl. padding: underuse-hysteresis signal
// Footprint including inter-chunk padding: the underuse-hysteresis signal.
s.miniBitmapUsed = span;
s.miniBitmapCapacity = s.miniBitmapChunkCount * MINI_BM_CHUNK_SIZE;
}
@@ -1129,9 +1122,9 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
s.miniMetadataOnly = metadataOnly;
s.miniHysteresisPending = !metadataOnly; // one hysteresis evaluation per rebuild
memset(&s.miniData, 0, sizeof(s.miniData));
// Mini bitmap is chunked (non-contiguous) so there is no single base pointer.
// SD mini glyphs are resolved via SdCardFont::miniGlyphBitmap() in the renderer;
// leave bitmap null so any accidental &bitmap[dataOffset] deref faults loudly.
// The mini bitmap is chunked (non-contiguous), so there is no single base
// pointer. Prewarmed SD glyphs are resolved via SdCardFont::miniGlyphBitmap()
// in the renderer; leave bitmap null so any stray &bitmap[dataOffset] faults.
s.miniData.bitmap = nullptr;
s.miniData.glyph = s.miniGlyphs;
s.miniData.intervals = s.miniIntervals;
@@ -1516,45 +1509,25 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
// Read bitmap data into temporary (if any)
uint8_t* tempBitmap = nullptr;
bool usingEmergency = false;
if (tempGlyph.dataLength > 0) {
tempBitmap = new (std::nothrow) uint8_t[tempGlyph.dataLength];
if (!tempBitmap) {
// Per-glyph malloc failed under heap pressure. Fall back to the pre-reserved
// emergency buffer so the glyph still renders instead of blanking (the whole
// last-loaded style, in practice bold-italic, otherwise disappears while BLE
// is resident). Read directly into it; this path bypasses the cache ring.
if (self->overflowEmergencyBuf_ && tempGlyph.dataLength <= self->overflowEmergencyCap_) {
tempBitmap = self->overflowEmergencyBuf_;
usingEmergency = true;
} else {
LOG_ERR("SDCF", "Overflow: failed to allocate %u bytes for U+%04X bitmap", tempGlyph.dataLength, codepoint);
return nullptr;
}
LOG_ERR("SDCF", "Overflow: failed to allocate %u bytes for U+%04X bitmap", tempGlyph.dataLength, codepoint);
return nullptr;
}
if (!file.seekSet(s.bitmapFileOffset + tempGlyph.dataOffset)) {
LOG_ERR("SDCF", "Overflow: failed to seek to bitmap for U+%04X", codepoint);
if (!usingEmergency) delete[] tempBitmap;
delete[] tempBitmap;
file.close();
return nullptr;
}
if (file.read(tempBitmap, tempGlyph.dataLength) != static_cast<int>(tempGlyph.dataLength)) {
LOG_ERR("SDCF", "Overflow: failed to read bitmap for U+%04X", codepoint);
if (!usingEmergency) delete[] tempBitmap;
delete[] tempBitmap;
return nullptr;
}
}
// Emergency-buffer path: return the glyph directly, without touching the ring
// (the buffer is a single shared slot, overwritten on the next miss).
if (usingEmergency) {
self->overflowEmergency_.glyph = tempGlyph;
self->overflowEmergency_.bitmap = tempBitmap;
self->overflowEmergency_.codepoint = codepoint;
self->overflowEmergency_.styleIdx = styleIdx;
return &self->overflowEmergency_.glyph;
}
// All reads succeeded — commit to slot and advance ring buffer
if (wasAtCapacity) {
delete[] self->overflow_[slot].bitmap;
@@ -1567,42 +1540,13 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
self->overflow_[slot].codepoint = codepoint;
self->overflow_[slot].styleIdx = styleIdx;
// No per-glyph log here: when the prewarm mini-data doesn't cover a page every
// glyph takes this path, which floods the console. Overflow-heavy pages are
// instead visible as slow renders.
LOG_DBG("SDCF", "Overflow: loaded U+%04X style %u on demand (slot %u/%u)", codepoint, styleIdx, slot,
OVERFLOW_CAPACITY);
return &self->overflow_[slot].glyph;
}
size_t SdCardFont::reportMemory() const {
size_t total = 0;
for (uint8_t si = 0; si < MAX_STYLES; ++si) {
const auto& s = styles_[si];
if (!s.present) continue;
size_t fixed = 0; // loaded once per family: interval/kern/lig tables
if (s.fullIntervals) fixed += s.header.intervalCount * sizeof(EpdUnicodeInterval);
if (s.bmpIntervals) fixed += s.header.intervalCount * sizeof(PerStyle::BmpInterval16);
if (s.kernLeftClasses) fixed += s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry);
if (s.kernRightClasses) fixed += s.header.kernRightEntryCount * sizeof(EpdKernClassEntry);
if (s.ligaturePairs) fixed += s.header.ligaturePairCount * sizeof(EpdLigaturePair);
// kept-if-fits mini arenas: capacity (not count) is what stays resident
size_t mini = s.miniIntervalCapacity * sizeof(EpdUnicodeInterval) + s.miniGlyphCapacity * sizeof(EpdGlyph) +
s.miniBitmapCapacity + s.miniKernLeftCapacity * sizeof(EpdKernClassEntry) +
s.miniKernRightCapacity * sizeof(EpdKernClassEntry) + s.miniKernMatrixCapacity;
const size_t adv = advanceTableSize_[si] * sizeof(AdvanceEntry);
LOG_DBG("SDCF", "mem style%u: fixed=%u mini=%u adv=%u", si, (unsigned)fixed, (unsigned)mini, (unsigned)adv);
total += fixed + mini + adv;
}
size_t overflowBytes = 0;
for (uint32_t i = 0; i < overflowCount_; ++i) {
if (overflow_[i].bitmap) overflowBytes += overflow_[i].glyph.dataLength;
}
total += overflowBytes + overflowCount_ * sizeof(OverflowEntry);
return total;
}
bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const {
if (glyph == &overflowEmergency_.glyph) return true;
for (uint32_t i = 0; i < overflowCount_; i++) {
if (&overflow_[i].glyph == glyph) return true;
}
@@ -1610,7 +1554,6 @@ bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const {
}
const uint8_t* SdCardFont::getOverflowBitmap(const EpdGlyph* glyph) const {
if (glyph == &overflowEmergency_.glyph) return overflowEmergency_.bitmap;
for (uint32_t i = 0; i < overflowCount_; i++) {
if (&overflow_[i].glyph == glyph) {
return overflow_[i].bitmap;
+18 -41
View File
@@ -94,12 +94,10 @@ class SdCardFont {
// Returns the bitmap for an on-demand-loaded (overflow) glyph.
const uint8_t* getOverflowBitmap(const EpdGlyph* glyph) const;
// Resolve a prewarmed mini glyph's bitmap. The mini bitmap is chunked (see
// MINI_BM_* above), so glyph->dataOffset is a byte offset across the fixed-size
// chunk sequence; each glyph lives wholly within one chunk. ctx is the glyph's
// EpdFontData::glyphMissCtx (an OverflowContext carrying the style index). Called
// by GfxRenderer::getGlyphBitmap for SD-card mini glyphs. Returns nullptr if the
// chunk isn't resident.
// Resolves a prewarmed mini glyph's chunked bitmap. `ctx` is the glyphMissCtx
// (an OverflowContext identifying the style); `dataOffset` is the glyph's
// virtual offset into the style's chunked arena. Returns nullptr if the chunk
// is absent or out of range. Called by GfxRenderer::getGlyphBitmap().
const uint8_t* miniGlyphBitmap(const void* ctx, uint32_t dataOffset) const;
// Extract SdCardFont* from an opaque glyphMissCtx pointer.
@@ -113,11 +111,6 @@ class SdCardFont {
uint32_t uniqueGlyphs = 0;
uint32_t bitmapBytes = 0;
};
// MEMFIX-PORT: SD font resident-bytes audit; portable
// Log per-style resident heap (full tables + kept-if-fits mini arenas +
// advance tables + overflow bitmaps) and return the total in bytes. Pure
// accounting — no allocation, no state change.
size_t reportMemory() const;
void logStats(const char* label = "SDCF");
void resetStats();
const Stats& getStats() const { return stats_; }
@@ -142,15 +135,17 @@ class SdCardFont {
uint8_t ligaturePairCount = 0;
};
// Per-page prewarm bitmap chunking. A page of a large 2-bpp font needs a
// 20-40 KB bitmap; as one contiguous block it fails to allocate on a fragmented,
// BLE-resident heap, dropping the whole style to the slow overflow path. Splitting
// it into fixed 4 KB chunks (each glyph kept whole within one chunk) lets it fit
// scattered free space, so prewarm succeeds and the render still gets a contiguous
// per-glyph pointer. Mirrors the image .pxc slot's chunking.
// The per-style mini bitmap arena is stored as a list of fixed-size chunks
// rather than one contiguous block. A whole page's 2bpp glyph bitmaps can run
// tens of KB; on a fragmented heap a single contiguous allocation of that size
// fails even when the same bytes are available as several smaller free blocks.
// Chunking lets the arena be assembled from blocks the allocator can actually
// provide. Each glyph's bitmap is placed wholly within one chunk (never
// straddling a boundary), so a glyph is addressed by a virtual offset that
// maps to (chunk index, offset-in-chunk) via miniGlyphBitmap().
static constexpr uint32_t MINI_BM_CHUNK_SHIFT = 12; // 4 KB chunks
static constexpr uint32_t MINI_BM_CHUNK_SIZE = 1u << MINI_BM_CHUNK_SHIFT;
static constexpr uint32_t MINI_BM_MAX_CHUNKS = 24; // 96 KB max per-style page bitmap
static constexpr uint32_t MINI_BM_MAX_CHUNKS = 24; // 96 KB ceiling per style/page
// All per-style data: file offsets, intervals, kern/lig, prewarm cache, EpdFont
struct PerStyle {
@@ -211,16 +206,15 @@ class SdCardFont {
EpdFontData miniData{};
EpdUnicodeInterval* miniIntervals = nullptr;
EpdGlyph* miniGlyphs = nullptr;
// Chunked mini bitmap (see MINI_BM_* above): fixed-size chunks so a large page's
// bitmap fits a fragmented heap. Each glyph is placed wholly within one chunk;
// EpdGlyph::dataOffset is a byte offset across the logical chunk sequence.
// Chunked mini bitmap arena (see MINI_BM_CHUNK_* above). Chunks are allocated
// on demand during prewarm; miniBitmapChunkCount is how many are live.
uint8_t* miniBitmapChunks[MINI_BM_MAX_CHUNKS] = {};
uint32_t miniBitmapChunkCount = 0; // chunks currently allocated (0..count-1), kept across pages
uint32_t miniBitmapChunkCount = 0;
uint32_t miniIntervalCount = 0;
uint32_t miniGlyphCount = 0;
uint32_t miniIntervalCapacity = 0;
uint32_t miniGlyphCapacity = 0;
uint32_t miniBitmapCapacity = 0; // miniBitmapChunkCount * MINI_BM_CHUNK_SIZE (hysteresis/report)
uint32_t miniBitmapCapacity = 0;
// Bitmap bytes the current page actually used (set by prewarmStyle), the
// underuse-hysteresis signal; 0 = no bitmap built this scope (metadata-only
// prewarm), which leaves the hysteresis counter untouched.
@@ -270,10 +264,7 @@ class SdCardFont {
};
OverflowContext overflowCtx_[MAX_STYLES] = {};
// Shared on-demand overflow ring (glyphs loaded via glyphMissHandler) -- the
// fallback for glyphs the per-page prewarm didn't cover. Kept small: the chunked
// mini-bitmap (below) lets prewarm succeed on a fragmented heap, so this ring
// only serves the rare uncovered glyph rather than a whole page.
// Shared on-demand overflow buffer (ring buffer of glyphs loaded via glyphMissHandler)
static constexpr uint32_t OVERFLOW_CAPACITY = 8;
struct OverflowEntry {
EpdGlyph glyph;
@@ -285,20 +276,6 @@ class SdCardFont {
uint32_t overflowCount_ = 0;
uint32_t overflowNext_ = 0;
// Single pre-reserved fallback buffer for the on-demand overflow path.
// onGlyphMiss() normally mallocs each glyph's bitmap; under heap pressure (BLE
// resident) that per-glyph malloc fails and the glyph -- and thus the whole
// last-loaded style, in practice bold-italic -- renders blank. This buffer is
// reserved once at load(), when the heap is healthy. When the per-glyph malloc
// fails, the current glyph is read into this buffer and rendered directly. It is
// NOT part of the cache ring above (it is overwritten on every use), which is
// fine: getGlyphBitmap() consumes the bitmap immediately after the miss handler
// returns, before the next glyph is fetched.
static constexpr uint32_t OVERFLOW_EMERGENCY_BYTES = 1024;
uint8_t* overflowEmergencyBuf_ = nullptr;
uint32_t overflowEmergencyCap_ = 0;
OverflowEntry overflowEmergency_ = {};
// Compact advance-only table for layout measurement (per-style).
// Built by buildAdvanceTable(), queried by getAdvance().
struct AdvanceEntry {
-8
View File
@@ -107,14 +107,6 @@ void SdCardFontManager::unloadAll(GfxRenderer& renderer) {
loadedPointSize_ = 0;
}
size_t SdCardFontManager::reportMemory() const {
size_t total = 0;
for (const auto& lf : loaded_) {
if (lf.font) total += lf.font->reportMemory();
}
return total;
}
int SdCardFontManager::getFontId(const std::string& familyName) const {
if (familyName != loadedFamilyName_ || loaded_.empty()) return 0;
return loaded_.front().fontId;
-4
View File
@@ -39,10 +39,6 @@ class SdCardFontManager {
// Get name of currently loaded family (empty if none).
const std::string& currentFamilyName() const { return loadedFamilyName_; };
// MEMFIX-PORT: font manager audit passthrough; portable
// Sum of loaded fonts' resident heap (see SdCardFont::reportMemory).
size_t reportMemory() const;
// Point size that was actually loaded.
// 0 if nothing loaded.
uint8_t currentPointSize() const { return loadedPointSize_; };
@@ -49,7 +49,6 @@ INTERVAL_PRESETS = {
"armenian": [(0x0530, 0x058F)],
"ethiopic": [(0x1200, 0x137F), (0x1380, 0x139F), (0x2D80, 0x2DDF)],
"vietnamese": [(0x01A0, 0x01B0), (0x1EA0, 0x1EF9)],
"ipa-chars": [(0x0250, 0x02AF), (0x02B0, 0x02FF)],
"punctuation": [(0x2000, 0x206F)],
"cjk": [(0x3000, 0x303F), (0x3040, 0x309F), (0x30A0, 0x30FF),
(0x4E00, 0x9FFF), (0xF900, 0xFAFF), (0xFF00, 0xFFEF)],
+21 -21
View File
@@ -35,7 +35,7 @@ families:
- name: Literata
description: "Screen-optimized serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/googlefonts/literata/main/fonts/ttf/Literata-Regular.ttf"}
@@ -45,7 +45,7 @@ families:
- name: SourceSerif4
description: "Adobe transitional serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-serif/release/TTF/SourceSerif4-Regular.ttf"}
@@ -55,7 +55,7 @@ families:
- name: NotoSerifExtended
description: "Serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/notofonts/NotoSerif/main/fonts/ttf/unhinted/instance_ttf/NotoSerif-Regular.ttf"}
@@ -65,7 +65,7 @@ families:
- name: Merriweather
description: "Warm serif for long-form reading (Latin, Cyrillic)"
intervals: latin-ext,cyrillic,ipa-chars
intervals: latin-ext,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/SorkinType/Merriweather/master/fonts/ttf/Merriweather-Regular.ttf"}
@@ -75,7 +75,7 @@ families:
- name: Lora
description: "Calligraphic serif for literary reading (Latin, Cyrillic)"
intervals: latin-ext,cyrillic,ipa-chars
intervals: latin-ext,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/main/fonts/ttf/Lora-Regular.ttf"}
@@ -85,7 +85,7 @@ families:
- name: GentiumBookPlus
description: "Scholarly serif with wide Unicode coverage (Latin, Greek, Cyrillic, IPA)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/gentiumbookplus/GentiumBookPlus-Regular.ttf"}
@@ -95,7 +95,7 @@ families:
- name: IBMPlexSerif
description: "Professional serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexserif/IBMPlexSerif-Regular.ttf"}
@@ -105,7 +105,7 @@ families:
- name: Bitter
description: "Slab serif designed for e-ink (Latin, Cyrillic)"
intervals: latin-ext,cyrillic,ipa-chars
intervals: latin-ext,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter%5Bwght%5D.ttf", variable: {wght: 500}}
@@ -115,7 +115,7 @@ families:
- name: Domitian
description: "A humanist serif for literary reading (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://mirrors.ctan.org/fonts/domitian/opentype/Domitian-Roman.otf"}
@@ -125,7 +125,7 @@ families:
- name: LibreBaskerville
description: "A serif reimplementing the classic Baskerville (Latin)"
intervals: latin-ext,ipa-chars
intervals: latin-ext
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Regular.ttf"}
@@ -135,7 +135,7 @@ families:
- name: Vollkorn
description: "A serif for bread and butter use by Friedrich Althausen (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 400}}
@@ -147,7 +147,7 @@ families:
- name: NotoSansExtended
description: "Sans-serif (Latin, Greek, Cyrillic, Georgian, Armenian, Ethiopic)"
intervals: latin-ext,greek,cyrillic,georgian,armenian,ethiopic,ipa-chars
intervals: latin-ext,greek,cyrillic,georgian,armenian,ethiopic
sizes: [12, 14, 16, 18]
styles:
regular: {path: "builtinFonts/source/NotoSans/NotoSans-Regular.ttf"}
@@ -157,7 +157,7 @@ families:
- name: Inter
description: "Modern sans-serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/inter/Inter%5Bopsz%2Cwght%5D.ttf", variable: {wght: 400, opsz: 14}}
@@ -167,7 +167,7 @@ families:
- name: SourceSans3
description: "Adobe humanist sans-serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-sans/release/TTF/SourceSans3-Regular.ttf"}
@@ -177,7 +177,7 @@ families:
- name: IBMPlexSans
description: "IBM corporate sans-serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/IBM/plex/master/packages/plex-sans/fonts/complete/ttf/IBMPlexSans-Regular.ttf"}
@@ -187,7 +187,7 @@ families:
- name: Alegreya
description: "Calligraphic serif/display (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/alegreya/Alegreya%5Bwght%5D.ttf", variable: {wght: 400}}
@@ -199,7 +199,7 @@ families:
- name: IBMPlexMono
description: "Monospace for code and technical reading (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexmono/IBMPlexMono-Regular.ttf"}
@@ -209,7 +209,7 @@ families:
- name: SourceCodePro
description: "Adobe monospace with excellent hinting (Latin)"
intervals: latin-ext,ipa-chars
intervals: latin-ext
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-code-pro/release/TTF/SourceCodePro-Regular.ttf"}
@@ -221,7 +221,7 @@ families:
- name: OpenDyslexic
description: "Dyslexia-friendly font (Latin)"
intervals: latin-ext,ipa-chars
intervals: latin-ext
sizes: [8, 10, 12, 14]
styles:
regular: {path: "builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf"}
@@ -233,7 +233,7 @@ families:
- name: AtkinsonHyperlegibleNext
description: "Accessibility font for low vision (Latin)"
intervals: latin-ext,ipa-chars
intervals: latin-ext
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/googlefonts/atkinson-hyperlegible-next/main/fonts/ttf/AtkinsonHyperlegibleNext-Regular.ttf"}
@@ -243,7 +243,7 @@ families:
- name: LexicaUltralegible
description: "Accessibility font for low vision / dyslexia (Latin)"
intervals: latin-ext,ipa-chars
intervals: latin-ext
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/jacobxperez/lexica-ultralegible/main/fonts/ttf/LexicaUltralegible-Regular.ttf"}
-12
View File
@@ -44,18 +44,6 @@ class Epub {
}
~Epub() = default;
std::string& getBasePath() { return contentBasePath; }
// MEMFIX-PORT: epub resident-bytes audit accessor; portable
// Approximate resident heap of the open book (audit): path strings, the CSS
// file list, and the parsed stylesheet. BookMetadataCache is file-backed
// (counts + HalFile handles) and contributes little.
size_t residentBytes() const {
size_t total = sizeof(Epub) + tocNcxItem.capacity() + tocNavItem.capacity() + filepath.capacity() +
contentBasePath.capacity() + cachePath.capacity();
for (const auto& f : cssFiles) total += sizeof(f) + (f.capacity() > 15 ? f.capacity() : 0);
if (cssParser) total += cssParser->residentBytes();
return total;
}
size_t cssRuleCount() const { return cssParser ? cssParser->ruleCount() : 0; }
bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
bool clearCache() const;
void setupCacheDir() const;
-4
View File
@@ -173,10 +173,6 @@ std::unique_ptr<Page> Page::deserialize(HalFile& file) {
uint16_t count;
serialization::readPod(file, count);
// Reserve up front: growth-by-doubling needs old + new capacity live at once and
// reallocates repeatedly — a field crash (bad_alloc -> abort under -fno-exceptions)
// hit exactly this append path on a heavily fragmented heap.
page->elements.reserve(count);
// Reserve up front so a page load costs one allocation for the element vector
// instead of a grow-copy-free cycle every doubling. `count` is untrusted (it
+83 -93
View File
@@ -681,76 +681,6 @@ static inline bool isCjkIdeograph(uint32_t cp) {
(cp >= 0x20000 && cp <= 0x3FFFF);
}
// The first word of a line may have its ruby characters wider than the word (the base text). In that case, we need to
// move the base text to the right a bit so that ruby text doesn't overflow the left border, and it is still centered
// over the base text. This function calculates how much we need to move the base text to the right.
int ParsedText::calculateRubyExtraStartOffset(const size_t wordIdx, const size_t maxWordIdx,
const GfxRenderer& renderer, const int fontId) const {
if (rubyTexts.empty() || wordIdx >= rubyTexts.size() || rubyTexts[wordIdx].empty() ||
(wordStyles[wordIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) {
return 0;
}
size_t groupWordCount = 1;
while (wordIdx + groupWordCount < maxWordIdx &&
(wordStyles[wordIdx + groupWordCount] & EpdFontFamily::RUBY_CONTINUE) != 0) {
groupWordCount++;
}
int groupActualWidth = 0;
for (size_t k = 0; k < groupWordCount; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[wordIdx + k], wordStyles[wordIdx + k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[wordIdx].c_str(), EpdFontFamily::SUP);
if (rubyWidth <= groupActualWidth) {
return 0;
}
const int leftOverlap = (rubyWidth - groupActualWidth) / 2;
// This function is only ever called for the first word of a line.
// words[wordIdx - 1], if it exists, is always the last word of the *prior* line
// and cannot absorb any left overhang on the current line.
// The full leftOverlap must therefore be reserved as a visual indent so the
// ruby text does not overflow the left margin.
return leftOverlap;
}
// The last ruby group on a line may have its ruby characters wider than the group's base text.
// The right half of that overhang protrudes past the last base character. This function returns
// the amount of right-margin space that must be reserved so the ruby does not overflow the right
// border. It mirrors calculateRubyExtraStartOffset: words[lineBreak] is on the *next* line and
// cannot absorb any of the right overhang on the current line, so the full rightOverlap is returned.
int ParsedText::calculateRubyExtraEndOffset(const size_t lineStartIdx, const size_t lineBreakIdx,
const GfxRenderer& renderer, const int fontId) const {
if (rubyTexts.empty() || lineBreakIdx == 0 || lineStartIdx >= lineBreakIdx) {
return 0;
}
// Walk backwards from the last word to find the leader of the last ruby group on the line.
size_t leaderIdx = lineBreakIdx - 1;
while (leaderIdx > lineStartIdx && (wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) {
leaderIdx--;
}
// leaderIdx must be a ruby group leader (non-empty ruby, no RUBY_CONTINUE flag).
if (leaderIdx >= rubyTexts.size() || rubyTexts[leaderIdx].empty() ||
(wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) {
return 0;
}
// Measure the group.
int groupActualWidth = 0;
for (size_t k = leaderIdx; k < lineBreakIdx; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[k], wordStyles[k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[leaderIdx].c_str(), EpdFontFamily::SUP);
if (rubyWidth <= groupActualWidth) {
return 0;
}
return (rubyWidth - groupActualWidth) / 2;
}
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
std::vector<uint16_t> wordWidths;
wordWidths.reserve(words.size());
@@ -761,6 +691,9 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
// Adjust widths for ruby groups to comply with JLReq standards
if (!rubyTexts.empty()) {
const int ascender = renderer.getFontAscenderSize(fontId);
const int maxOverhang = ascender / 2; // Maximum overhang over non-ideographic characters (Kana, punctuation, etc.)
struct RubyGroupInfo {
size_t start;
size_t count;
@@ -797,10 +730,9 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
if (g.start > 0) {
const uint32_t cpPrev = lastCodepoint(words[g.start - 1]);
if (isCjkIdeograph(cpPrev)) {
wordWidths[g.start - 1] += g.leftOverlap;
wordWidths[g.start] += g.leftOverlap;
} else {
const int maxLeftOverhang = wordWidths[g.start - 1] / 2;
wordWidths[g.start - 1] += std::max(0, g.leftOverlap - maxLeftOverhang);
wordWidths[g.start] += std::max(0, g.leftOverlap - maxOverhang);
}
}
@@ -820,8 +752,7 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
if (isCjkIdeograph(cpNext)) {
wordWidths[g.start + g.count - 1] += g.rightOverlap;
} else {
const int maxRightOverhang = wordWidths[nextIdx] / 2;
wordWidths[g.start + g.count - 1] += std::max(0, g.rightOverlap - maxRightOverhang);
wordWidths[g.start + g.count - 1] += std::max(0, g.rightOverlap - maxOverhang);
}
// Check if there is another ruby group further ahead separated only by non-ideographs
@@ -838,10 +769,8 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
gapWidth += wordWidths[k];
}
if (onlyNonIdeographsInBetween) {
const int maxRightOverhang = wordWidths[g.start + g.count - 1] / 2;
const int maxLeftOverhang = wordWidths[nextG.start - 1] / 2;
const int allowedRight = std::min(g.rightOverlap, maxRightOverhang);
const int allowedLeft = std::min(nextG.leftOverlap, maxLeftOverhang);
const int allowedRight = std::min(g.rightOverlap, maxOverhang);
const int allowedLeft = std::min(nextG.leftOverlap, maxOverhang);
const int touchOverlap = allowedRight + allowedLeft - gapWidth;
if (touchOverlap > 0) {
wordWidths[g.start + g.count - 1] += touchOverlap;
@@ -908,7 +837,36 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
}
// Calculate extraStartOffset for the first word on the line (i) (protect left margin)
const int extraStartOffset = (j == i) ? calculateRubyExtraStartOffset(i, totalWordCount, renderer, fontId) : 0;
int extraStartOffset = 0;
if (j == i && !rubyTexts.empty() && i < rubyTexts.size() && !rubyTexts[i].empty() &&
(wordStyles[i] & EpdFontFamily::RUBY_CONTINUE) == 0) {
int groupWordCount = 1;
while (i + groupWordCount < totalWordCount &&
(wordStyles[i + groupWordCount] & EpdFontFamily::RUBY_CONTINUE) != 0) {
groupWordCount++;
}
int groupActualWidth = 0;
for (int k = 0; k < groupWordCount; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[i + k], wordStyles[i + k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[i].c_str(), EpdFontFamily::SUP);
if (rubyWidth > groupActualWidth) {
const int leftOverlap = (rubyWidth - groupActualWidth) / 2;
const int ascender = renderer.getFontAscenderSize(fontId);
const int maxOverhang = ascender / 2;
int expansion_added = 0;
if (i > 0) {
const uint32_t cpPrev = lastCodepoint(words[i - 1]);
if (isCjkIdeograph(cpPrev)) {
expansion_added = leftOverlap;
} else {
expansion_added = std::max(0, leftOverlap - maxOverhang);
}
}
extraStartOffset = leftOverlap - expansion_added;
}
}
currlen += wordWidths[j] + gap + (j == i ? extraStartOffset : 0);
@@ -921,7 +879,46 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
continue;
}
const int extraEndOffset = calculateRubyExtraEndOffset(i, j + 1, renderer, fontId);
// Calculate extraEndOffset if we break after word j (protect right margin)
int extraEndOffset = 0;
if (!rubyTexts.empty() && j < rubyTexts.size()) {
bool isLastOfGroup = false;
size_t leaderIdx = j;
while (true) {
if (!rubyTexts[leaderIdx].empty() && (wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) == 0) {
isLastOfGroup = true;
break;
}
if (leaderIdx == 0 || (wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) == 0) {
break;
}
leaderIdx--;
}
if (isLastOfGroup && (j + 1 == totalWordCount || (wordStyles[j + 1] & EpdFontFamily::RUBY_CONTINUE) == 0)) {
int groupActualWidth = 0;
for (size_t k = leaderIdx; k <= j; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[k], wordStyles[k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[leaderIdx].c_str(), EpdFontFamily::SUP);
if (rubyWidth > groupActualWidth) {
const int rightOverlap = (rubyWidth - groupActualWidth) / 2;
const int ascender = renderer.getFontAscenderSize(fontId);
const int maxOverhang = ascender / 2;
int expansion_added = 0;
if (j + 1 < totalWordCount) {
const uint32_t cpNext = firstCodepoint(words[j + 1]);
if (isCjkIdeograph(cpNext)) {
expansion_added = rightOverlap;
} else {
expansion_added = std::max(0, rightOverlap - maxOverhang);
}
}
extraEndOffset = rightOverlap - expansion_added;
}
}
}
if (currlen + extraEndOffset > effectivePageWidth) {
continue; // Cannot split here as it would overflow the right margin
@@ -942,9 +939,7 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
}
}
// Favor longer lines when line-breaking costs are equal, to avoid unnecessary short lines in Chinese and Japanese
// text.
if (cost <= dp[i]) {
if (cost < dp[i]) {
dp[i] = cost;
ans[i] = j; // j is the index of the last word in this optimal line
}
@@ -1176,9 +1171,6 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
std::copy(rubyTexts.begin() + lastBreakAt, rubyTexts.begin() + lastBreakAt + copyCount, lineRubyTexts.begin());
}
const int extraStartOffset = calculateRubyExtraStartOffset(lastBreakAt, lineBreak, renderer, fontId);
const int extraEndOffset = calculateRubyExtraEndOffset(lastBreakAt, lineBreak, renderer, fontId);
std::vector<std::string> lineWords;
lineWords.reserve(lineWordCount);
std::vector<EpdFontFamily::Style> lineWordStyles;
@@ -1233,9 +1225,8 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
? CssTextAlign::Right
: blockStyle.alignment;
// For justified text, compute per-gap extra to distribute remaining space evenly.
// extraEndOffset reserves space for any ruby group at the right edge of the line.
const int spareSpace = effectivePageWidth - extraStartOffset - extraEndOffset - lineWordWidthSum - totalNaturalGaps;
// For justified text, compute per-gap extra to distribute remaining space evenly
const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
? computeJustifyExtra(spareSpace, actualGapCount)
: 0;
@@ -1316,8 +1307,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
}
}
const int reorderedSpare =
effectivePageWidth - extraStartOffset - extraEndOffset - reorderedWordWidthSum - reorderedNaturalGaps;
const int reorderedSpare = effectivePageWidth - reorderedWordWidthSum - reorderedNaturalGaps;
const int reorderedJustifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
? computeJustifyExtra(reorderedSpare, reorderedGapCount)
: 0;
@@ -1422,7 +1412,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
}
} else {
// LTR: position words from left to right
int xpos = firstLineIndent + extraStartOffset;
int xpos = firstLineIndent;
if (effectiveAlignment == CssTextAlign::Right) {
xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
} else if (effectiveAlignment == CssTextAlign::Center) {
-3
View File
@@ -59,9 +59,6 @@ class ParsedText {
void pushVisibleOffset(uint32_t offset);
void insertVisibleOffset(size_t wordIndex, uint32_t offset);
void eraseVisibleOffsetPrefix(size_t count);
int calculateRubyExtraStartOffset(size_t wordIdx, size_t maxWordIdx, const GfxRenderer& renderer, int fontId) const;
int calculateRubyExtraEndOffset(size_t lineStartIdx, size_t lineBreakIdx, const GfxRenderer& renderer,
int fontId) const;
int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const;
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
+1 -1
View File
@@ -31,7 +31,7 @@ namespace {
// match. Keeps <br>-per-paragraph books (common CJK formatting) from
// re-adding container spacing at every paragraph.
// v35: Persist a uint32_t visible-text start offset for every page.
constexpr uint8_t SECTION_FILE_VERSION = 36;
constexpr uint8_t SECTION_FILE_VERSION = 35;
// Written into the version field while a build is in progress; patched to
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
-15
View File
@@ -72,7 +72,6 @@ class Section {
// Builds write here and are swapped over filePath only on commit, so a prior
// partial/finalized file stays readable while a rebuild is in progress.
std::string binTmpPath() const { return filePath + ".part"; }
std::unique_ptr<Page> loadPageAt(int page) const;
// Read a page already laid out by the in-progress build (page < build LUT size), from
// the partially-written tmp .bin without disturbing the build's write cursor.
@@ -125,20 +124,6 @@ class Section {
// (covers finalized sections and partials from a previous session).
std::optional<uint16_t> findAnchor(const std::string& anchor) const;
// MEMFIX-PORT: section resident-bytes audit accessor; portable
// Approximate resident heap for the audit log. Steady state (no build) a
// Section holds little beyond itself; during a build the page LUT and path
// strings dominate (the parser's internal footprint is not walked here).
size_t residentBytes() const {
size_t total = sizeof(Section) + filePath.capacity();
if (build_) {
total += sizeof(BuildContext) + build_->lut.capacity() * sizeof(PageLutEntry) +
build_->parsePath.capacity() + build_->contentBase.capacity() + build_->imageBasePath.capacity() +
build_->htmlPath.capacity() + build_->tmpHtmlPath.capacity();
}
return total;
}
// True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a
// giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild.
bool hasHtmlCache() const;
-19
View File
@@ -327,25 +327,6 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
FontCacheManager* fcm = renderer.getFontCacheManager();
if (fcm && fcm->isScanning()) return;
// Grayscale strip passes re-invoke render() once per horizontal band and once
// per plane. When the image's pixels are resident in the RAM slot, redrawing
// from RAM is cheap and preserves 4-level AA. When they are NOT resident -- the
// payload was too big to cache on this heap (a full-page image is ~80 KB vs
// ~48 KB free with BLE up), or another image on the page owns the single slot --
// each band would re-stream the entire .pxc from SD. For a full-page image
// that is ~13 full-file reads (BW pass + ~6 column bands x 2 planes in
// portrait), the dominant cost of an image page (measured bw_render ~7.8 s).
// The image already has its 1bpp rendering from the BW pass, so skip the
// grayscale redraw instead of re-streaming: cacheable images keep full AA,
// oversized ones stay BW (already dithered in the .pxc). Orientation-agnostic --
// gated on slot residency, not on Portrait vs Landscape.
const GfxRenderer::RenderMode renderMode = renderer.getRenderMode();
if (renderMode == GfxRenderer::GRAYSCALE_LSB || renderMode == GfxRenderer::GRAYSCALE_MSB) {
if (pxcSlotHash != imagePathHash(getCachePath(imagePath))) {
return; // not slot-resident; leave the BW-pass pixels untouched
}
}
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
const int screenWidth = renderer.getScreenWidth();
+60 -6
View File
@@ -135,32 +135,86 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
const bool scanning = renderer.isFontCacheScanning();
const int ascender = renderer.getFontAscenderSize(fontId);
// Resolve ruby positions. Layout (extractLine) has already reserved extraStartOffset on the
// left and extraEndOffset on the right, so the centered rubyX is always within the page margins.
// Resolve ruby collisions left-to-right to prevent adjacent ruby texts from overlapping
struct RubyDrawInfo {
int x;
int width;
std::string text;
BidiUtils::BidiBaseDir baseDir;
};
// hasRuby() is an O(numWords) scan, so resolve it once here rather than per word.
// Both arrays below are only ever read when the line carries ruby, so they stay
// empty (zero allocations) for the ruby-less case, which is every line of a
// non-CJK book. Sized lazily inside the branch.
const bool blockHasRuby = hasRuby();
std::vector<int> wordShiftArr;
std::vector<RubyDrawInfo> rubies;
if (blockHasRuby) {
wordShiftArr.assign(numWords, 0);
rubies.resize(numWords);
int accumulatedShift = 0;
int lastEnd = -9999;
for (uint16_t i = 0; i < numWords; i++) {
wordShiftArr[i] = accumulatedShift;
if (i < rubyTexts.size() && !rubyTexts[i].empty() && (wordStyle(i) & EpdFontFamily::RUBY_CONTINUE) == 0) {
// Find the group size (how many words are part of this ruby annotation)
int groupWordCount = 1;
while (i + groupWordCount < numWords && (wordStyle(i + groupWordCount) & EpdFontFamily::RUBY_CONTINUE) != 0) {
groupWordCount++;
}
// Compute actual width for the group
int groupActualWidth = 0;
for (int k = 0; k < groupWordCount; ++k) {
groupActualWidth += renderer.getTextAdvanceX(fontId, wordText(i + k), wordStyle(i + k));
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[i].c_str(), EpdFontFamily::SUP);
const char* word = wordText(i);
const int leaderWordX = xposArr[i] + x;
const int leaderWordX_shifted = leaderWordX + accumulatedShift;
const auto baseDir =
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(wordText(i), blockStyle.isRtl ? 1 : 0));
rubies[i] = {leaderWordX - (rubyWidth - groupActualWidth) / 2, rubyTexts[i], baseDir};
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(word, blockStyle.isRtl ? 1 : 0));
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[i].c_str(), EpdFontFamily::SUP);
const int screenWidth = renderer.getScreenWidth();
int rubyX = 0;
int groupDrawX = 0;
if (rubyWidth > groupActualWidth) {
rubyX = leaderWordX_shifted - (rubyWidth - groupActualWidth) / 2;
if (i == 0) {
rubyX = std::max(leaderWordX_shifted, rubyX);
}
if (rubyX < lastEnd) {
rubyX = lastEnd;
}
groupDrawX = rubyX + (rubyWidth - groupActualWidth) / 2;
} else {
groupDrawX = leaderWordX_shifted;
rubyX = groupDrawX + (groupActualWidth - rubyWidth) / 2;
if (i == 0) {
rubyX = std::max(leaderWordX_shifted, rubyX);
}
if (rubyX < lastEnd) {
const int push = lastEnd - rubyX;
rubyX = lastEnd;
groupDrawX += push;
}
}
rubyX = std::max(0, std::min(rubyX, screenWidth - rubyWidth));
// Keep groupDrawX aligned if rubyX was clamped by screen edges
if (rubyWidth > groupActualWidth) {
groupDrawX = rubyX + (rubyWidth - groupActualWidth) / 2;
}
rubies[i] = {rubyX, rubyWidth, rubyTexts[i], baseDir};
lastEnd = rubyX + rubyWidth;
// Propagate shift to all words in the group and subsequent words
const int groupShift = groupDrawX - leaderWordX;
accumulatedShift = groupShift;
for (int k = 0; k < groupWordCount; ++k) {
wordShiftArr[i + k] = accumulatedShift;
}
i += groupWordCount - 1;
}
}
@@ -221,7 +275,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
wordY += ascender / 4;
}
const int drawX = wordX;
const int drawX = wordX + (blockHasRuby ? wordShiftArr[i] : 0);
if (boundary > 0) {
// Focus split: draw bold prefix, then the regular suffix at a pre-computed x offset.
-14
View File
@@ -67,20 +67,6 @@ class CssParser {
*/
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
// MEMFIX-PORT: stylesheet resident-bytes audit accessor; portable
// Approximate resident heap of the parsed stylesheet, for the audit log.
// unordered_map cost model: bucket array + one node per rule (libstdc++ node
// overhead ~= 2 pointers + hash) + key string capacity when it exceeds SSO.
size_t residentBytes() const {
size_t total = rulesBySelector_.bucket_count() * sizeof(void*);
for (const auto& kv : rulesBySelector_) {
total += sizeof(void*) * 2 + sizeof(size_t); // node overhead
total += sizeof(kv);
if (kv.first.capacity() > 15) total += kv.first.capacity(); // beyond SSO
}
return total;
}
/**
* Check if any rules have been loaded
*/
+23 -38
View File
@@ -62,22 +62,16 @@ void FontCacheManager::resetStats() {
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) {
if (!text) return;
// Bucket the text under its base style so each style is prewarmed only for what
// it actually renders (see the header comment on scanText_).
scanText_ += text;
if (scanFontId_ < 0) scanFontId_ = fontId;
const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03;
size_t& len = scanTextLen_[baseStyle];
const size_t remaining = (len < SCAN_TEXT_CAPACITY - 1) ? (SCAN_TEXT_CAPACITY - 1 - len) : 0;
if (remaining > 0) {
const size_t textLen = strnlen(text, remaining);
memcpy(scanText_[baseStyle] + len, text, textLen);
len += textLen;
scanText_[baseStyle][len] = '\0';
}
if (!scanRecorded_) {
scanFontId_ = fontId; // capture the first drawn font; may be negative (SD font)
scanRecorded_ = true;
const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
uint32_t cpCount = 0;
while (*p) {
if ((*p & 0xC0) != 0x80) cpCount++;
p++;
}
scanStyleCounts_[baseStyle] += cpCount;
}
// --- PrewarmScope implementation ---
@@ -86,37 +80,28 @@ FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manage
manager_->scanMode_ = ScanMode::Scanning;
manager_->clearCache();
manager_->resetStats();
for (uint8_t i = 0; i < STYLE_COUNT; i++) {
manager_->scanTextLen_[i] = 0;
manager_->scanText_[i][0] = '\0';
}
manager_->scanText_.clear();
manager_->scanText_.reserve(2048); // Pre-allocate to avoid heap fragmentation from repeated concat
memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_));
manager_->scanFontId_ = -1;
manager_->scanRecorded_ = false;
}
void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
manager_->scanMode_ = ScanMode::None;
if (!manager_->scanRecorded_) return; // nothing recorded (also the "already ran" no-op)
if (manager_->scanText_.empty()) return;
// Prewarm each style separately, against only the text drawn in that style, so a
// secondary style's page-slot buffer stays as small as its actual usage. A single
// per-style bit mask routes prewarmCache() to exactly that style on both the SD
// and compressed-font paths.
for (uint8_t i = 0; i < STYLE_COUNT; i++) {
if (manager_->scanTextLen_[i] == 0) continue;
const unsigned long tStyle = millis();
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_[i], static_cast<uint8_t>(1u << i));
// Per-style prewarm cost + text volume: on a heap-pressured build (BLE resident)
// a cold chapter-opening page can touch several styles, each paying SD reads for
// bitmaps + kern/ligature tables. This line attributes a slow page to the styles
// and byte volume actually loaded, so the wall-clock spike is not a guess.
LOG_DBG("FCM", "prewarm style%u: %lums (%u text bytes)", i, millis() - tStyle,
(unsigned)manager_->scanTextLen_[i]);
manager_->scanTextLen_[i] = 0;
manager_->scanText_[i][0] = '\0';
// Build style bitmask from all styles that appeared during the scan
uint8_t styleMask = 0;
for (uint8_t i = 0; i < 4; i++) {
if (manager_->scanStyleCounts_[i] > 0) styleMask |= (1 << i);
}
manager_->scanFontId_ = -1;
manager_->scanRecorded_ = false; // makes the dtor's second call a no-op
if (styleMask == 0) styleMask = 1; // default to regular
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_.c_str(), styleMask);
// Free scan string memory
manager_->scanText_.clear();
manager_->scanText_.shrink_to_fit();
}
FontCacheManager::PrewarmScope::~PrewarmScope() {
Loaded 20 of 103 files, more files were not shown because too many files have changed in this diff. Show more