mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3c2d7b099 | ||
|
|
bcb8a74fad | ||
|
|
2c8bd8ea1a | ||
|
|
dd2ec5b525 | ||
|
|
28af4189d4 | ||
|
|
f45f75f966 | ||
|
|
4564c3e743 | ||
|
|
1373bef1c6 | ||
|
|
7dbfcbb52d | ||
|
|
981e73c137 | ||
|
|
200abc6aa4 | ||
|
|
f59f72fed3 | ||
|
|
f3a4b98711 | ||
|
|
4af4a6ad44 | ||
|
|
d656fec87a | ||
|
|
9b16a8e263 | ||
|
|
f6566bcc74 | ||
|
|
a86a644adc | ||
|
|
02398c8a96 | ||
|
|
ba92cb902e | ||
|
|
4d19d38152 | ||
|
|
302c0771dd | ||
|
|
c1a396c1ba | ||
|
|
078d9ef535 | ||
|
|
17e4230068 | ||
|
|
34e7ed249b | ||
|
|
cecdbefa0e | ||
|
|
4c5fd653c0 | ||
|
|
05c1e9aa46 | ||
|
|
f3da5e4f06 | ||
|
|
6f0707e83c | ||
|
|
7b6df60a54 | ||
|
|
1c13913713 | ||
|
|
613371b716 | ||
|
|
0d7a84e3c5 | ||
|
|
4c93950771 | ||
|
|
1d01eebf55 | ||
|
|
9bf6e5f43c | ||
|
|
ad3138983b | ||
|
|
793685e76b | ||
|
|
551d29744a | ||
|
|
74a0969cc6 | ||
|
|
06aa1ac2f7 | ||
|
|
c463da994e | ||
|
|
988652513a | ||
|
|
28d0d6f5d2 | ||
|
|
568831f232 | ||
|
|
d6f5be6b7a | ||
|
|
d30fde2f4d | ||
|
|
6305777b22 | ||
|
|
9ab0b0bfb7 |
No files matched your search
+1
-1
Submodule freeink-sdk updated: a485dc46ef...8b8337b586.
@@ -369,6 +369,11 @@ 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;
|
||||
|
||||
+140
-28
@@ -99,8 +99,11 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
||||
s.miniIntervals = nullptr;
|
||||
delete[] s.miniGlyphs;
|
||||
s.miniGlyphs = nullptr;
|
||||
delete[] s.miniBitmap;
|
||||
s.miniBitmap = nullptr;
|
||||
for (auto& chunk : s.miniBitmapChunks) {
|
||||
delete[] chunk;
|
||||
chunk = nullptr;
|
||||
}
|
||||
s.miniBitmapChunkCount = 0;
|
||||
s.miniIntervalCount = 0;
|
||||
s.miniGlyphCount = 0;
|
||||
s.miniIntervalCapacity = 0;
|
||||
@@ -188,6 +191,10 @@ 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]);
|
||||
}
|
||||
@@ -704,6 +711,12 @@ 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_);
|
||||
@@ -1010,31 +1023,65 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
totalBitmapSize += s.miniGlyphs[i].dataLength;
|
||||
}
|
||||
|
||||
if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) {
|
||||
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
s.miniBitmapUsed = totalBitmapSize; // underuse-hysteresis signal for resetStyleMiniData
|
||||
|
||||
// Read bitmap data sorted by file offset
|
||||
// Read bitmap data sorted by file offset (sequential glyphs then avoid a seek).
|
||||
std::sort(readOrder, readOrder + validCount,
|
||||
[&](uint32_t a, uint32_t b) { return s.miniGlyphs[a].dataOffset < s.miniGlyphs[b].dataOffset; });
|
||||
|
||||
uint32_t miniBitmapOffset = 0;
|
||||
// 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.
|
||||
uint32_t span = 0;
|
||||
uint32_t lastBitmapEnd = UINT32_MAX;
|
||||
for (uint32_t i = 0; i < validCount; i++) {
|
||||
uint32_t mapIdx = readOrder[i];
|
||||
const uint32_t mapIdx = readOrder[i];
|
||||
EpdGlyph& glyph = s.miniGlyphs[mapIdx];
|
||||
const uint32_t len = glyph.dataLength;
|
||||
|
||||
if (glyph.dataLength == 0) {
|
||||
glyph.dataOffset = miniBitmapOffset;
|
||||
if (len == 0) {
|
||||
glyph.dataOffset = span;
|
||||
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.
|
||||
LOG_ERR("SDCF", "Prewarm: glyph %u B exceeds chunk %u B (style %u)", len, MINI_BM_CHUNK_SIZE, styleIdx);
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
|
||||
uint32_t fileOff = s.bitmapFileOffset + glyph.dataOffset;
|
||||
// Avoid straddling a chunk boundary.
|
||||
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;
|
||||
}
|
||||
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);
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
if (!s.miniBitmapChunks[chunkIdx]) {
|
||||
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);
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
if (chunkIdx + 1 > s.miniBitmapChunkCount) s.miniBitmapChunkCount = chunkIdx + 1;
|
||||
}
|
||||
|
||||
const uint32_t off = span & (MINI_BM_CHUNK_SIZE - 1);
|
||||
const 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);
|
||||
@@ -1046,18 +1093,20 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
}
|
||||
seekCount++;
|
||||
}
|
||||
if (file.read(s.miniBitmap + miniBitmapOffset, glyph.dataLength) != static_cast<int>(glyph.dataLength)) {
|
||||
if (file.read(s.miniBitmapChunks[chunkIdx] + off, len) != static_cast<int>(len)) {
|
||||
LOG_ERR("SDCF", "Prewarm: short bitmap read (style %u)", styleIdx);
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
lastBitmapEnd = fileOff + glyph.dataLength;
|
||||
lastBitmapEnd = fileOff + len;
|
||||
|
||||
glyph.dataOffset = miniBitmapOffset;
|
||||
miniBitmapOffset += glyph.dataLength;
|
||||
glyph.dataOffset = span;
|
||||
span += len;
|
||||
}
|
||||
s.miniBitmapUsed = span; // footprint incl. padding: underuse-hysteresis signal
|
||||
s.miniBitmapCapacity = s.miniBitmapChunkCount * MINI_BM_CHUNK_SIZE;
|
||||
}
|
||||
|
||||
uint32_t sdTime = millis() - sdStart;
|
||||
@@ -1080,7 +1129,10 @@ 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));
|
||||
s.miniData.bitmap = s.miniBitmap;
|
||||
// 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.
|
||||
s.miniData.bitmap = nullptr;
|
||||
s.miniData.glyph = s.miniGlyphs;
|
||||
s.miniData.intervals = s.miniIntervals;
|
||||
s.miniData.intervalCount = s.miniIntervalCount;
|
||||
@@ -1464,25 +1516,45 @@ 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) {
|
||||
LOG_ERR("SDCF", "Overflow: failed to allocate %u bytes for U+%04X bitmap", tempGlyph.dataLength, codepoint);
|
||||
return nullptr;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
if (!file.seekSet(s.bitmapFileOffset + tempGlyph.dataOffset)) {
|
||||
LOG_ERR("SDCF", "Overflow: failed to seek to bitmap for U+%04X", codepoint);
|
||||
delete[] tempBitmap;
|
||||
if (!usingEmergency) 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);
|
||||
delete[] tempBitmap;
|
||||
if (!usingEmergency) 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;
|
||||
@@ -1495,13 +1567,42 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
||||
self->overflow_[slot].codepoint = codepoint;
|
||||
self->overflow_[slot].styleIdx = styleIdx;
|
||||
|
||||
LOG_DBG("SDCF", "Overflow: loaded U+%04X style %u on demand (slot %u/%u)", codepoint, styleIdx, slot,
|
||||
OVERFLOW_CAPACITY);
|
||||
// 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.
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1509,6 +1610,7 @@ 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;
|
||||
@@ -1517,4 +1619,14 @@ const uint8_t* SdCardFont::getOverflowBitmap(const EpdGlyph* glyph) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint8_t* SdCardFont::miniGlyphBitmap(const void* ctx, uint32_t dataOffset) const {
|
||||
const auto* octx = static_cast<const OverflowContext*>(ctx);
|
||||
const PerStyle& s = styles_[octx->styleIdx];
|
||||
const uint32_t chunkIdx = dataOffset >> MINI_BM_CHUNK_SHIFT;
|
||||
if (chunkIdx >= s.miniBitmapChunkCount) return nullptr;
|
||||
const uint8_t* chunk = s.miniBitmapChunks[chunkIdx];
|
||||
if (!chunk) return nullptr;
|
||||
return chunk + (dataOffset & (MINI_BM_CHUNK_SIZE - 1));
|
||||
}
|
||||
|
||||
SdCardFont* SdCardFont::fromMissCtx(void* ctx) { return static_cast<OverflowContext*>(ctx)->self; }
|
||||
@@ -94,6 +94,14 @@ 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.
|
||||
const uint8_t* miniGlyphBitmap(const void* ctx, uint32_t dataOffset) const;
|
||||
|
||||
// Extract SdCardFont* from an opaque glyphMissCtx pointer.
|
||||
// Used by GfxRenderer::getGlyphBitmap() to recover the SdCardFont from EpdFontData::glyphMissCtx.
|
||||
static SdCardFont* fromMissCtx(void* ctx);
|
||||
@@ -105,6 +113,11 @@ 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_; }
|
||||
@@ -129,6 +142,16 @@ 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.
|
||||
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
|
||||
|
||||
// All per-style data: file offsets, intervals, kern/lig, prewarm cache, EpdFont
|
||||
struct PerStyle {
|
||||
CpFontHeader header{};
|
||||
@@ -188,12 +211,16 @@ class SdCardFont {
|
||||
EpdFontData miniData{};
|
||||
EpdUnicodeInterval* miniIntervals = nullptr;
|
||||
EpdGlyph* miniGlyphs = nullptr;
|
||||
uint8_t* miniBitmap = 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.
|
||||
uint8_t* miniBitmapChunks[MINI_BM_MAX_CHUNKS] = {};
|
||||
uint32_t miniBitmapChunkCount = 0; // chunks currently allocated (0..count-1), kept across pages
|
||||
uint32_t miniIntervalCount = 0;
|
||||
uint32_t miniGlyphCount = 0;
|
||||
uint32_t miniIntervalCapacity = 0;
|
||||
uint32_t miniGlyphCapacity = 0;
|
||||
uint32_t miniBitmapCapacity = 0;
|
||||
uint32_t miniBitmapCapacity = 0; // miniBitmapChunkCount * MINI_BM_CHUNK_SIZE (hysteresis/report)
|
||||
// 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.
|
||||
@@ -243,7 +270,10 @@ class SdCardFont {
|
||||
};
|
||||
OverflowContext overflowCtx_[MAX_STYLES] = {};
|
||||
|
||||
// Shared on-demand overflow buffer (ring buffer of glyphs loaded via glyphMissHandler)
|
||||
// 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.
|
||||
static constexpr uint32_t OVERFLOW_CAPACITY = 8;
|
||||
struct OverflowEntry {
|
||||
EpdGlyph glyph;
|
||||
@@ -255,6 +285,20 @@ 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 {
|
||||
|
||||
@@ -107,6 +107,14 @@ 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;
|
||||
|
||||
@@ -39,6 +39,10 @@ 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_; };
|
||||
|
||||
@@ -44,6 +44,18 @@ 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;
|
||||
|
||||
@@ -173,6 +173,10 @@ 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
|
||||
|
||||
@@ -72,6 +72,7 @@ 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.
|
||||
@@ -124,6 +125,20 @@ 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;
|
||||
|
||||
@@ -327,6 +327,25 @@ 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();
|
||||
|
||||
@@ -67,6 +67,20 @@ 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
|
||||
*/
|
||||
|
||||
@@ -62,16 +62,22 @@ void FontCacheManager::resetStats() {
|
||||
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
|
||||
|
||||
void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) {
|
||||
scanText_ += text;
|
||||
if (scanFontId_ < 0) scanFontId_ = fontId;
|
||||
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_).
|
||||
const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03;
|
||||
const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
|
||||
uint32_t cpCount = 0;
|
||||
while (*p) {
|
||||
if ((*p & 0xC0) != 0x80) cpCount++;
|
||||
p++;
|
||||
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;
|
||||
}
|
||||
scanStyleCounts_[baseStyle] += cpCount;
|
||||
}
|
||||
|
||||
// --- PrewarmScope implementation ---
|
||||
@@ -80,28 +86,37 @@ FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manage
|
||||
manager_->scanMode_ = ScanMode::Scanning;
|
||||
manager_->clearCache();
|
||||
manager_->resetStats();
|
||||
manager_->scanText_.clear();
|
||||
manager_->scanText_.reserve(2048); // Pre-allocate to avoid heap fragmentation from repeated concat
|
||||
memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_));
|
||||
for (uint8_t i = 0; i < STYLE_COUNT; i++) {
|
||||
manager_->scanTextLen_[i] = 0;
|
||||
manager_->scanText_[i][0] = '\0';
|
||||
}
|
||||
manager_->scanFontId_ = -1;
|
||||
manager_->scanRecorded_ = false;
|
||||
}
|
||||
|
||||
void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
|
||||
manager_->scanMode_ = ScanMode::None;
|
||||
if (manager_->scanText_.empty()) return;
|
||||
if (!manager_->scanRecorded_) return; // nothing recorded (also the "already ran" no-op)
|
||||
|
||||
// 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);
|
||||
// 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';
|
||||
}
|
||||
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();
|
||||
manager_->scanFontId_ = -1;
|
||||
manager_->scanRecorded_ = false; // makes the dtor's second call a no-op
|
||||
}
|
||||
|
||||
FontCacheManager::PrewarmScope::~PrewarmScope() {
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
class FontDecompressor;
|
||||
class SdCardFont;
|
||||
@@ -51,7 +51,22 @@ class FontCacheManager {
|
||||
|
||||
enum class ScanMode : uint8_t { None, Scanning };
|
||||
ScanMode scanMode_ = ScanMode::None;
|
||||
std::string scanText_;
|
||||
uint32_t scanStyleCounts_[4] = {};
|
||||
// Per-style scan buffers (index = base Style: 0=Regular, 1=Bold, 2=Italic,
|
||||
// 3=Bold-Italic). Each page render prewarms every style ONLY against the text
|
||||
// actually drawn in that style, not the whole page. Prewarming a secondary style
|
||||
// over the entire page produced a full-page-sized FontDecompressor page-slot
|
||||
// buffer per style; with the BLE stack resident the later styles' malloc failed,
|
||||
// dropping bold/italic onto the hot-group fallback (which also OOM'd) so those
|
||||
// glyphs rendered blank. Bucketing by style keeps each secondary buffer as small
|
||||
// as the bold/italic text present, so every style reliably gets its slot.
|
||||
static constexpr size_t SCAN_TEXT_CAPACITY = 2048;
|
||||
static constexpr uint8_t STYLE_COUNT = 4;
|
||||
char scanText_[STYLE_COUNT][SCAN_TEXT_CAPACITY] = {};
|
||||
size_t scanTextLen_[STYLE_COUNT] = {};
|
||||
// Font id recorded during the scan pass. SD-card font ids are negative
|
||||
// (std::hash-derived), so "nothing recorded" CANNOT be sentinelled as < 0 --
|
||||
// that would swallow every SD font and skip its prewarm entirely. Track
|
||||
// whether anything was recorded with an explicit flag instead.
|
||||
int scanFontId_ = -1;
|
||||
bool scanRecorded_ = false;
|
||||
};
|
||||
@@ -81,6 +81,9 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
|
||||
if (sdFont->isOverflowGlyph(glyph)) {
|
||||
return sdFont->getOverflowBitmap(glyph); // may be nullptr for zero-width glyphs
|
||||
}
|
||||
// Prewarmed SD glyph: the mini bitmap is chunked (non-contiguous), so there is
|
||||
// no fontData->bitmap base to index. Resolve via the per-style chunk table.
|
||||
return sdFont->miniGlyphBitmap(fontData->glyphMissCtx, glyph->dataOffset);
|
||||
}
|
||||
return &fontData->bitmap[glyph->dataOffset];
|
||||
}
|
||||
|
||||
@@ -324,6 +324,25 @@ STR_HW_BACK_LABEL: "Back (1st button)"
|
||||
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
|
||||
STR_HW_LEFT_LABEL: "Left (3rd button)"
|
||||
STR_HW_RIGHT_LABEL: "Right (4th button)"
|
||||
STR_BLUETOOTH: "Bluetooth"
|
||||
STR_TOGGLE_BLUETOOTH: "Toggle Bluetooth"
|
||||
STR_BT_SCAN_PAIR: "Scan & Pair"
|
||||
STR_BT_NO_DEVICES: "No devices found"
|
||||
STR_BT_FREE_HINT1: "Set Free2/3 to Reader Mode and"
|
||||
STR_BT_FREE_HINT2: "Volume Function to pair"
|
||||
STR_BT_DISCONNECT: "Disconnect"
|
||||
STR_BT_CONNECTED_TO: "Connected: %s"
|
||||
STR_BT_NOT_CONNECTED: "Not connected"
|
||||
STR_BT_PAIRED_DEVICES: "Paired Devices"
|
||||
STR_BT_NO_PAIRED: "No paired devices"
|
||||
STR_BT_MAP_BUTTONS: "Map Remote Buttons"
|
||||
STR_BT_PRESS_REMOTE: "Press a button on your remote"
|
||||
STR_BT_CONNECTING_POPUP: "BT Connecting..."
|
||||
STR_BT_PAUSED_LOW_MEM_POPUP: "BT paused (low memory)"
|
||||
STR_STATE_PAUSED: "PAUSED"
|
||||
STR_BT_PAGE_FORWARD: "Page Forward"
|
||||
STR_BT_PAGE_BACK: "Page Back"
|
||||
STR_BT_FORGET_PROMPT: "Hold Confirm to forget"
|
||||
STR_GO_TO_PERCENT: "Go to %"
|
||||
STR_GO_HOME_BUTTON: "Go Home"
|
||||
STR_SYNC_PROGRESS: "Sync Progress"
|
||||
|
||||
+84
-6
@@ -60,6 +60,16 @@ build_flags =
|
||||
-Wno-bidi-chars
|
||||
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity
|
||||
-fno-exceptions
|
||||
# FreeInk panel profiles: compile both X3 (792x528/UC8253) and X4 (800x480/SSD1677);
|
||||
# the firmware picks the active one at runtime via HalDisplay setDisplayX3().
|
||||
-DFREEINK_DEVICE_X3=1
|
||||
-DFREEINK_DEVICE_X4=1
|
||||
# BLE HID page-turner host (BleKeyboardHost). NimBLE role/bond config is baked into
|
||||
# the prebuilt arduino-esp32 framework's sdkconfig.h, so we don't redefine it here
|
||||
# (doing so only warns and has no effect). The host only compiles when
|
||||
# FREEINK_CAP_BLE_HID_HOST is set on the env (below); with the capability off,
|
||||
# BleKeyboardHost links stubs and pulls in zero NimBLE code.
|
||||
-DFREEINK_BLE_HID_SHOW_UNNAMED_DEVICES=0
|
||||
|
||||
build_unflags =
|
||||
-std=gnu++11
|
||||
@@ -70,18 +80,50 @@ board_build.flash_mode = dio
|
||||
board_build.flash_size = 16MB
|
||||
board_build.partitions = partitions.csv
|
||||
|
||||
; MEMFIX-PORT: custom_sdkconfig heap reclamation (~32-37 KB). Rebuilds the
|
||||
; Arduino core libs on first build (slower once, cached after; needs the CMake
|
||||
; pin in platformio.local.ini on macOS).
|
||||
; Shrink the NimBLE footprint for a 1-connection HID host moving 3-6 byte reports.
|
||||
; Field-measured: begin() costs ~52 KB with these trims vs ~68 KB with the prebuilt
|
||||
; framework defaults — and that 15 KB is the difference between the stack landing
|
||||
; above the reader's render shed floor (stable coexistence) and below it (a
|
||||
; guaranteed shed/restart flap). Rebuilds the Arduino core libs on first build
|
||||
; (slower once, cached after; needs the CMake pin in platformio.local.ini on macOS).
|
||||
custom_sdkconfig =
|
||||
; NimBLE-Arduino supplies the host stack; ESP-IDF supplies only the controller.
|
||||
; Enabling IDF's NimBLE host too produces duplicate npl_freertos_* symbols.
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=n
|
||||
CONFIG_BT_CONTROLLER_ONLY=y
|
||||
CONFIG_BT_CONTROLLER_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=n
|
||||
CONFIG_BT_NIMBLE_ROLE_BROADCASTER=n
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||
CONFIG_BT_NIMBLE_MAX_CCCDS=2
|
||||
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=23
|
||||
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=6
|
||||
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=6 ; was 24 x 320 B
|
||||
CONFIG_BT_NIMBLE_ACL_BUF_COUNT=6 ; was 24 x 255 B
|
||||
CONFIG_BT_NIMBLE_HCI_EVT_HI_BUF_COUNT=12 ; was 30 x 70 B; only scan bursts need many
|
||||
; IDF 5.5 sizes the HCI transport pools under TRANSPORT_* names; pin both spellings.
|
||||
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=6
|
||||
CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=12
|
||||
CONFIG_BT_NIMBLE_ATT_MAX_PREP_ENTRIES=4 ; was 64; a HID host never does prepared writes
|
||||
CONFIG_BT_CTRL_BLE_MAX_ACT=3 ; was 6; need conn + scan + initiate only
|
||||
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=50 ; was 100; pairing-time scan only
|
||||
CONFIG_BT_CTRL_ADV_DUP_FILT_MAX=10 ; was 30
|
||||
; BLE Mesh defaults ON for mesh-capable SoCs, but this firmware is a HID host,
|
||||
; not a mesh node. Worse, IDF's esp_ble_mesh guards BLE_MESH_ADV_TASK_CORE
|
||||
; behind CONFIG_BT_BLUEDROID_ENABLED / CONFIG_BT_NIMBLE_ENABLED (mesh/kernel.h);
|
||||
; in our controller-only build (both IDF host stacks off, host is
|
||||
; NimBLE-Arduino) that macro is undefined and adv_common.c fails to compile.
|
||||
; Disable mesh outright: fixes the build and drops the whole unused component.
|
||||
CONFIG_BLE_MESH=n
|
||||
;
|
||||
; If an interrupted rebuild fails with "multiple definition of 'app_main'"
|
||||
; (stale generated scaffold), clean it up with:
|
||||
; rm -rf .dummy CMakeLists.txt sdkconfig.default sdkconfig.defaults .pio/build/default
|
||||
; Do NOT use `git clean -fdX` — it deletes platformio.local.ini.
|
||||
custom_sdkconfig =
|
||||
; Task stack right-sizing from measured high-water marks (heap block map +
|
||||
; per-task stack audit, July 2026): esp_timer used ~0.8 KB of 8 KB across
|
||||
; every capture; the FreeRTOS timer service used ~0.5 KB
|
||||
; every capture incl. BLE sessions; the FreeRTOS timer service used ~0.5 KB
|
||||
; of 4 KB. Neither runs TLS or app code. ~7 KB back to the heap.
|
||||
CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096
|
||||
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2560
|
||||
@@ -121,6 +163,7 @@ extra_scripts =
|
||||
pre:scripts/gen_i18n.py
|
||||
pre:scripts/git_branch.py
|
||||
pre:scripts/patch_jpegdec.py
|
||||
pre:scripts/patch_bt_mem.py
|
||||
post:scripts/register_unit_tests_target.py
|
||||
|
||||
; Libraries
|
||||
@@ -129,6 +172,7 @@ lib_deps =
|
||||
InputManager=symlink://freeink-sdk/libs/hardware/InputManager
|
||||
EInkDisplay=symlink://freeink-sdk/libs/display/FreeInkDisplay
|
||||
SDCardManager=symlink://freeink-sdk/libs/hardware/SDCardManager
|
||||
; FreeInk HAL support libs the above depend on (BoardConfig pin maps, etc.).
|
||||
BoardConfig=symlink://freeink-sdk/libs/hardware/BoardConfig
|
||||
XteinkDetect=symlink://freeink-sdk/libs/hardware/XteinkDetect
|
||||
PowerManager=symlink://freeink-sdk/libs/hardware/PowerManager
|
||||
@@ -144,11 +188,24 @@ lib_deps =
|
||||
links2004/WebSockets @ 2.7.3
|
||||
wolfssl/Arduino-wolfSSL @ 5.7.2
|
||||
|
||||
; Ignore Arduino's legacy BLE wrapper. The pioarduino core builder preserves
|
||||
; ESP-IDF's `bt` controller because NimBLE-Arduino is present in lib_deps.
|
||||
lib_ignore =
|
||||
BLE
|
||||
|
||||
; BLE HID host libraries are declared per-env, not in [base].lib_deps, on purpose:
|
||||
; a developer's platformio.local.ini replaces [base].lib_deps wholesale (to repoint
|
||||
; the FreeInk SDK symlinks at a local checkout), which silently dropped these and
|
||||
; broke the build with "BleKeyboardHost.h: No such file". Env-level lib_deps are NOT
|
||||
; overridden by local.ini, so declaring them here makes them stick. BleKeyboardHost
|
||||
; is needed by EVERY env (src/BleInput compiles unconditionally against its stubs);
|
||||
; NimBLE-Arduino is added only where FREEINK_CAP_BLE_HID_HOST is set.
|
||||
[env:default]
|
||||
extends = base
|
||||
lib_deps =
|
||||
${base.lib_deps}
|
||||
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
|
||||
h2zero/NimBLE-Arduino @ ^2.3.8
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
; CROSSPOINT_VERSION is set by scripts/git_branch.py (includes branch + short SHA)
|
||||
@@ -156,10 +213,17 @@ build_flags =
|
||||
-DFREEINK_DEVICE_X3=1
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=2 ; Set log level to debug for development builds
|
||||
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
|
||||
-DFREEINK_BLE_HID_SCAN_DEBUG=1 ; verbose BLE scan lifecycle/advertisement logs for bring-up
|
||||
-DFREEINK_BLE_HID_REPORT_DEBUG=1 ; raw HID report hex dumps + report-map hints (bring-up)
|
||||
|
||||
|
||||
[env:gh_release]
|
||||
extends = base
|
||||
lib_deps =
|
||||
${base.lib_deps}
|
||||
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
|
||||
h2zero/NimBLE-Arduino @ ^2.3.8
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DFREEINK_DEVICE_X4=1
|
||||
@@ -167,9 +231,14 @@ build_flags =
|
||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=1 ; Set log level to info for release builds
|
||||
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
|
||||
|
||||
[env:gh_release_rc]
|
||||
extends = base
|
||||
lib_deps =
|
||||
${base.lib_deps}
|
||||
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
|
||||
h2zero/NimBLE-Arduino @ ^2.3.8
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DFREEINK_DEVICE_X4=1
|
||||
@@ -177,9 +246,14 @@ build_flags =
|
||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
|
||||
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
|
||||
|
||||
[env:slim]
|
||||
extends = base
|
||||
; Cap off: BleKeyboardHost links stubs (src/BleInput still compiles), no NimBLE.
|
||||
lib_deps =
|
||||
${base.lib_deps}
|
||||
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DFREEINK_DEVICE_X4=1
|
||||
@@ -197,6 +271,10 @@ build_flags =
|
||||
extends = base
|
||||
board = esp32-s3-devkitc1-n16r8
|
||||
board_build.mcu = esp32s3
|
||||
; Cap off: BleKeyboardHost links stubs (src/BleInput still compiles), no NimBLE.
|
||||
lib_deps =
|
||||
${base.lib_deps}
|
||||
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DFREEINK_DEVICE_STICKY=1
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
PlatformIO pre-build script: make the arduino-esp32 core define _btLibraryInUse
|
||||
in a BLE-controller-only build.
|
||||
|
||||
The core header cores/esp32/esp32-hal-bt-mem.h emits a constructor
|
||||
(_setBtLibraryInUse) that references `_btLibraryInUse` whenever BLE hardware is
|
||||
present, but the core only DEFINES that symbol (in esp32-hal-bt.c) when an IDF
|
||||
host stack -- CONFIG_BLUEDROID_ENABLED or CONFIG_NIMBLE_ENABLED -- is enabled.
|
||||
|
||||
CrossPoint runs the ESP-IDF BLE *controller* only, with NimBLE-Arduino supplying
|
||||
the host (both IDF host stacks are disabled in custom_sdkconfig to avoid duplicate
|
||||
npl_freertos_* symbols). In that configuration the core never defines the symbol,
|
||||
so NimBLE's constructor link-fails with "undefined reference to `_btLibraryInUse'"
|
||||
-- and it fails in links that application source cannot reach (the final firmware
|
||||
link and the arduino-lib-builder core-rebuild's dummy firmware).
|
||||
|
||||
Fix: turn the header's `extern bool _btLibraryInUse;` DECLARATION into a weak
|
||||
DEFINITION. Every translation unit that includes the header (NimBLE) then provides
|
||||
the symbol itself; the linker merges the duplicate weak defs, and the core's own
|
||||
strong definition still wins whenever a host stack is actually enabled. It must
|
||||
live in the header, not our src, because only the header is seen by NimBLE's TU in
|
||||
every link.
|
||||
|
||||
Idempotent text edit. Patching bumps the header mtime, so NimBLE recompiles
|
||||
against it on the next build.
|
||||
"""
|
||||
|
||||
Import("env") # noqa: F821 (SCons-injected global)
|
||||
import glob
|
||||
import os
|
||||
|
||||
EXTERN_DECL = "extern bool _btLibraryInUse;"
|
||||
MARKER = "patch_bt_mem.py"
|
||||
WEAK_DEF = (
|
||||
"__attribute__((weak)) bool _btLibraryInUse = false; /* CrossPoint " + MARKER + " */"
|
||||
)
|
||||
|
||||
|
||||
def _find_header(env):
|
||||
rel = os.path.join("cores", "esp32", "esp32-hal-bt-mem.h")
|
||||
candidates = []
|
||||
|
||||
# 1) Ask the platform for the framework package dir (works when the name matches).
|
||||
try:
|
||||
fw = env.PioPlatform().get_package_dir("framework-arduinoespressif32") # noqa: F821
|
||||
if fw:
|
||||
candidates.append(os.path.join(fw, rel))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2) Glob the packages dir(s) -- robust to the exact package name/version.
|
||||
pkg_dirs = []
|
||||
for key in ("PROJECT_PACKAGES_DIR", "PROJECT_CORE_DIR"):
|
||||
try:
|
||||
d = env.subst("$%s" % key) # noqa: F821
|
||||
if d:
|
||||
pkg_dirs.append(d)
|
||||
except Exception:
|
||||
pass
|
||||
pkg_dirs.append(os.path.expanduser(os.path.join("~", ".platformio", "packages")))
|
||||
for base in pkg_dirs:
|
||||
pattern = os.path.join(base, "**", "framework-arduinoespressif32*", rel)
|
||||
candidates.extend(glob.glob(pattern, recursive=True))
|
||||
|
||||
for c in candidates:
|
||||
if os.path.isfile(c):
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def patch_bt_mem(env):
|
||||
header = _find_header(env)
|
||||
if not header:
|
||||
print("patch_bt_mem: esp32-hal-bt-mem.h not found; skipping (controller-only "
|
||||
"link may fail on _btLibraryInUse)")
|
||||
return
|
||||
|
||||
with open(header, "r") as f:
|
||||
text = f.read()
|
||||
|
||||
if MARKER in text:
|
||||
print("patch_bt_mem: already patched (%s)" % header)
|
||||
return
|
||||
if EXTERN_DECL not in text:
|
||||
print("patch_bt_mem: '%s' not found in %s; NOT patching (upstream header "
|
||||
"changed -- update this script)" % (EXTERN_DECL, header))
|
||||
return
|
||||
|
||||
text = text.replace(EXTERN_DECL, WEAK_DEF, 1)
|
||||
with open(header, "w") as f:
|
||||
f.write(text)
|
||||
print("patch_bt_mem: patched %s (_btLibraryInUse now weak-defined)" % header)
|
||||
|
||||
|
||||
patch_bt_mem(env) # noqa: F821
|
||||
@@ -0,0 +1,121 @@
|
||||
#include "BleInput.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalPowerManager.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
|
||||
namespace bleinput {
|
||||
|
||||
namespace {
|
||||
volatile bool g_startInProgress = false;
|
||||
}
|
||||
|
||||
// NimBLE controller init/deinit hang (interrupt WDT) if run at the 10 MHz low-power
|
||||
// frequency, so force normal CPU speed around both. Centralized here so every caller
|
||||
// (boot restore, settings toggle, reader toggle, sleep) is covered automatically.
|
||||
bool ensureStarted() {
|
||||
g_startInProgress = true;
|
||||
HalPowerManager::Lock powerLock;
|
||||
const bool ok = BleHid.begin(kHostName);
|
||||
g_startInProgress = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool startInProgress() { return g_startInProgress; }
|
||||
|
||||
// Full teardown (NimBLE deinit), not just a link drop, so the BLE stack's RAM is
|
||||
// returned to the heap — otherwise memory-hungry work like EPUB inflate can't
|
||||
// allocate even after the user turns Bluetooth off.
|
||||
void stop() {
|
||||
HalPowerManager::Lock powerLock;
|
||||
BleHid.end();
|
||||
}
|
||||
|
||||
bool encodeKey(const freeink::KeyEvent& ev, uint8_t& kind, uint8_t& value) {
|
||||
if (ev.special != freeink::SpecialKey::None) {
|
||||
kind = 0;
|
||||
value = static_cast<uint8_t>(ev.special);
|
||||
return true;
|
||||
}
|
||||
if (ev.keycode != 0) {
|
||||
kind = 1;
|
||||
value = ev.keycode;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
const char* specialName(uint8_t value) {
|
||||
switch (static_cast<freeink::SpecialKey>(value)) {
|
||||
case freeink::SpecialKey::Enter:
|
||||
return "Enter";
|
||||
case freeink::SpecialKey::Backspace:
|
||||
return "Backspace";
|
||||
case freeink::SpecialKey::Tab:
|
||||
return "Tab";
|
||||
case freeink::SpecialKey::Escape:
|
||||
return "Escape";
|
||||
case freeink::SpecialKey::Delete:
|
||||
return "Delete";
|
||||
case freeink::SpecialKey::Left:
|
||||
return "Left";
|
||||
case freeink::SpecialKey::Right:
|
||||
return "Right";
|
||||
case freeink::SpecialKey::Up:
|
||||
return "Up";
|
||||
case freeink::SpecialKey::Down:
|
||||
return "Down";
|
||||
case freeink::SpecialKey::Home:
|
||||
return "Home";
|
||||
case freeink::SpecialKey::End:
|
||||
return "End";
|
||||
case freeink::SpecialKey::PageUp:
|
||||
return "Page Up";
|
||||
case freeink::SpecialKey::PageDown:
|
||||
return "Page Down";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void showConnectingUntilLinked(const GfxRenderer& renderer, const MappedInputManager& input) {
|
||||
if (!BleHid.isRunning() || BleHid.isConnected()) return;
|
||||
// drawPopup refreshes the panel itself, so draw once and let e-ink hold it while we
|
||||
// pump the host. Holds until the remote links, the user presses a button to bail, or
|
||||
// a generous timeout (a remote that slept after a disconnect needs a button to wake).
|
||||
GUI.drawPopup(renderer, tr(STR_BT_CONNECTING_POPUP));
|
||||
const unsigned long deadline = millis() + 10000;
|
||||
while (!BleHid.isConnected() && millis() < deadline) {
|
||||
BleHid.poll();
|
||||
input.update();
|
||||
if (input.wasAnyPressed()) break;
|
||||
delay(50);
|
||||
}
|
||||
// Note: the caller must redraw to clear the popup. For grayscale reader pages the
|
||||
// caller should also request a ghost-cleanup (HALF) refresh first — a plain fast/
|
||||
// partial refresh ghosts badly over the BW popup (see Activity::requestGhostCleanup).
|
||||
}
|
||||
|
||||
void describeKey(uint8_t kind, uint8_t value, char* out, size_t outLen) {
|
||||
if (!out || outLen == 0) return;
|
||||
if (kind == 0) {
|
||||
const char* name = specialName(value);
|
||||
if (name) {
|
||||
strncpy(out, name, outLen - 1);
|
||||
out[outLen - 1] = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Printable ASCII usage handled as a generic key code; show the raw value.
|
||||
snprintf(out, outLen, "Key 0x%02X", static_cast<unsigned>(value));
|
||||
}
|
||||
|
||||
} // namespace bleinput
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
// CrossPoint <-> FreeInk BLE HID host glue.
|
||||
//
|
||||
// Thin, capability-safe helpers around freeink::BleKeyboardHost (the `BleHid`
|
||||
// singleton). When FREEINK_CAP_BLE_HID_HOST is compiled out the SDK links stubs,
|
||||
// so every call here is still valid and simply no-ops / returns false — callers
|
||||
// need no #ifdefs.
|
||||
//
|
||||
// The (kind, value) pair produced by encodeKey() is the stable identity stored in
|
||||
// CrossPointSettings::bleKeyMap. Page-turner remotes emit "special" keys
|
||||
// (PageUp/PageDown/arrows); plain keyboards emit usage codes. We deliberately
|
||||
// ignore modifiers and the printable char for matching (page turners don't use
|
||||
// modifiers), keeping the persisted entry a trivial two-byte comparison.
|
||||
|
||||
#include <BleKeyboardHost.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class GfxRenderer;
|
||||
class MappedInputManager;
|
||||
|
||||
namespace bleinput {
|
||||
|
||||
// Advertised central name shown to peripherals during pairing.
|
||||
inline constexpr const char* kHostName = "CrossPoint";
|
||||
|
||||
// Heap floor for starting the NimBLE stack (measured begin() cost: ~52-57 KB).
|
||||
// This must leave the reader usable AFTER the stack is resident, not merely fit the
|
||||
// stack itself. Framebuffer lending covers a section build's one big inflate window,
|
||||
// but NOT its storm of small allocations (CSS/sscanf/strings) which draw from the
|
||||
// general heap; at 56 KB the stack started with ~2 KB general heap left and the next
|
||||
// build abort()ed inside sscanf (We_baseline from a cleared cache, BLE connected).
|
||||
// 80 KB is reachable at steady-state reading (~84 KB idle with an SD font + section)
|
||||
// and leaves ~26 KB general heap after startup; the build pre-flight
|
||||
// (BUILD_MIN_FREE_HEAP) frees BLE and retries when a build still needs more, and the
|
||||
// render shed (RENDER_MIN_FREE_HEAP) backstops tight renders.
|
||||
inline constexpr size_t kStartMinFreeHeap = 80 * 1024;
|
||||
|
||||
// Lower floor for the Bluetooth settings screen, where the user has explicitly asked
|
||||
// for BLE right now (scanning/pairing is dead without the stack). No page renders or
|
||||
// section builds run there, so the reader-sized reserve above doesn't apply — only
|
||||
// NimBLE's own ~57 KB plus working margin.
|
||||
inline constexpr size_t kStartMinFreeHeapExplicit = 70 * 1024;
|
||||
|
||||
// Start the BLE HID host (idempotent). Returns false if BLE is compiled out or
|
||||
// NimBLE init failed. Safe to call repeatedly.
|
||||
bool ensureStarted();
|
||||
bool startInProgress();
|
||||
|
||||
// Drop the active link (e.g. before deep sleep or when the user disables BT).
|
||||
void stop();
|
||||
|
||||
// Encode a decoded key event into the stable (kind, value) identity used by the
|
||||
// settings map. kind: 0 = SpecialKey, 1 = HID usage. Returns false when the event
|
||||
// carries no usable identity (no special key and no usage code).
|
||||
bool encodeKey(const freeink::KeyEvent& ev, uint8_t& kind, uint8_t& value);
|
||||
|
||||
// Human-readable name for a stored (kind, value) identity, for the mapping UI.
|
||||
// Writes a null-terminated string into out (e.g. "Page Down", "Key 0x4B").
|
||||
void describeKey(uint8_t kind, uint8_t value, char* out, size_t outLen);
|
||||
|
||||
// Draw a "BT Connecting..." popup and pump the BLE host until the bonded remote
|
||||
// links, the user presses a button to dismiss, or a timeout. No-op if BLE isn't
|
||||
// running or is already connected. The caller must redraw afterward to clear it.
|
||||
void showConnectingUntilLinked(const GfxRenderer& renderer, const MappedInputManager& input);
|
||||
|
||||
} // namespace bleinput
|
||||
@@ -0,0 +1,19 @@
|
||||
// Weak fallback definition of `_btLibraryInUse` for the BLE-controller-only build.
|
||||
//
|
||||
// The arduino-esp32 core header cores/esp32/esp32-hal-bt-mem.h emits a global
|
||||
// constructor (`_setBtLibraryInUse`) that references `_btLibraryInUse` whenever BLE
|
||||
// hardware is present, but the core only DEFINES that symbol (in esp32-hal-bt.c)
|
||||
// when an IDF host stack — CONFIG_BLUEDROID_ENABLED or CONFIG_NIMBLE_ENABLED — is
|
||||
// enabled. CrossPoint runs the ESP-IDF BLE *controller* only, with NimBLE-Arduino
|
||||
// supplying the host (both IDF host stacks are disabled in custom_sdkconfig), so the
|
||||
// core never defines it and both the core lib and NimBLEDevice.cpp.o fail the final
|
||||
// firmware link with "undefined reference to `_btLibraryInUse'".
|
||||
//
|
||||
// scripts/patch_bt_mem.py turns the header's declaration into a weak definition, but
|
||||
// that framework file gets reset by package reinstalls and the custom_sdkconfig core
|
||||
// rebuild, so the patch does not always survive to compile time. This weak definition
|
||||
// lives in our own always-linked source and resolves the reference in firmware.elf
|
||||
// regardless of the header's state. It is `weak`, so if a host stack is ever enabled
|
||||
// (giving the core its own strong definition), that strong symbol wins with no clash;
|
||||
// and it coexists with the header's weak def when the patch did apply.
|
||||
extern "C" __attribute__((weak)) bool _btLibraryInUse = false;
|
||||
Loaded 20 of 53 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user