Compare commits

..
Author SHA1 Message Date
Justin Mitchell bf9fea5923 Fix image decode overflow and viewport clipping issues
Prevent integer overflow when validating large image dimensions by using 64-bit arithmetic. Add periodic yielding during JPEG/PNG decode to prevent watchdog timeouts on multi-second operations. Clamp image top margins to prevent full-height images from overflowing page bottom and failing bounds checks. Increase max source pixels from 3MP to 8MP to support high-resolution ebook covers while keeping decode times reasonable.
2026-08-10 14:48:47 -04:00
William Floyd e9c3a166f5 feat: StarDict .syn synonym lookup support (#2696) 2026-08-10 20:01:28 +03:00
Justin Mitchell d720891b2f feat: Adds dark mode with image polarity preservation (#2705) 2026-08-10 19:58:22 +03:00
e4aae7baef feat: retain custom sleep screen during wake (#2943)
Co-authored-by: Bridger Brundy <bridgerborpcc@gmail.com>
Co-authored-by: Julia <julia@uxj.io>
2026-08-10 17:02:20 +03:00
Uri Tauber 7f8b005ae1 chore: update AI agents instructions 2026-08-10 16:06:51 +03:00
Serena 81028d5849 fix(ui): wrap button hint labels that overflow their button (#2928) 2026-08-10 12:05:27 +03:00
Justin Mitchell 0423276d2c fix: accept any 2xx from KOSync-compatible servers (#2945) 2026-08-10 09:26:37 +03:00
Leopoldo Pla Sempere 596f0d3f71 fix: invalidate stale footnote caches (#2935) 2026-08-09 19:27:48 +03:00
hooligan333andErica Jensen 6af9a04934 perf: cache cumulative spine sizes in RAM (#2441)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
2026-08-09 17:31:42 +03:00
François Chaix 1582e70ef4 fix(epub): bump FOOTNOTE_HREF_LEN 96→256 for long calibre paths (#2722) 2026-08-09 17:23:40 +03:00
Uri Tauber d4733bfc65 chore: split GH build action (#2901)
## Summary

Build action take too much time.
Lets see if splitting it to two seperate action would speed things up.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< PARTIALLY >**_
2026-08-09 03:55:33 -04:00
114 changed files with 1622 additions and 2954 deletions

No files matched your search

+52 -8
View File
@@ -63,7 +63,8 @@ jobs:
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
build:
build-default:
name: Build default
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
@@ -90,14 +91,10 @@ jobs:
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build CrossPoint
# Both envs in ONE pio invocation: a second `pio run` can wipe the whole
# .pio/build tree (project-checksum mismatch after the S3 toolchain
# installs into the restored cache), deleting the default firmware.bin
# before the artifact upload.
- name: Build default firmware
run: |
set -euo pipefail
pio run -e default -e sticky | tee pio.log
pio run -e default | tee pio.log
- name: Extract firmware stats
@@ -120,6 +117,52 @@ jobs:
path: .pio/build/default/firmware.bin
if-no-files-found: error
build-sticky:
name: Build sticky
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build sticky firmware
run: |
set -euo pipefail
pio run -e sticky | tee pio.log
- name: Extract firmware stats
run: |
set -euo pipefail
ram_line="$(grep -E "RAM:\\s" -m1 pio.log || true)"
flash_line="$(grep -E "Flash:\\s" -m1 pio.log || true)"
echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT"
echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT"
{
echo "## Sticky firmware build stats"
if [ -n "$ram_line" ]; then echo "- ${ram_line}"; else echo "- RAM: not found"; fi
if [ -n "$flash_line" ]; then echo "- ${flash_line}"; else echo "- Flash: not found"; fi
} >> "$GITHUB_STEP_SUMMARY"
unit-tests:
runs-on: ubuntu-latest
steps:
@@ -152,7 +195,8 @@ jobs:
test-status:
name: Test Status
needs:
- build
- build-default
- build-sticky
- clang-format
- cppcheck
- unit-tests
-47
View File
@@ -1,47 +0,0 @@
name: Compile Release
on:
push:
tags:
- '*'
jobs:
build-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build CrossPoint
run: pio run -e gh_release
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: CrossPoint-${{ github.ref_name }}
path: |
.pio/build/gh_release/bootloader.bin
.pio/build/gh_release/firmware.bin
.pio/build/gh_release/firmware.elf
.pio/build/gh_release/firmware.map
.pio/build/gh_release/partitions.bin
+1
View File
@@ -31,3 +31,4 @@ lib/EpdFont/scripts/output/
/dependencies.lock
/sdkconfig.*
sdkconfig.sticky
flake.*
+179 -68
View File
File diff suppressed because it is too large. Load diff
Symlink
+1
View File
@@ -0,0 +1 @@
/home/urit/Documents/CrossPoint/crosspoint-reader/.skills/SKILL.md
+6 -4
View File
@@ -10,9 +10,10 @@ A dictionary folder must contain:
- `.idx` — word index (required, **must be uncompressed** — a `.idx.gz` will not work; decompress it on your computer with `gzip -d` first)
- `.dict` or `.dict.dz` — definition data (`.dict.dz` is supported as-is; entries are decompressed on the fly during lookup)
- `.syn` — synonym index (optional; maps alternate spellings and irregular forms to their headword)
- `.ifo` — metadata (optional)
Not supported: `.syn` synonym files (ignored), dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
Not supported: dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
## Setting Up a Dictionary
@@ -35,13 +36,14 @@ One word on the page becomes highlighted:
2. Press **Confirm** to look up the highlighted word.
3. Press **Back** to return to the reader.
On the very first lookup with a dictionary (and again if the dictionary file changes), the reader shows *"Indexing dictionary…"* while it builds a small `.qidx` sidecar file next to the `.idx`. This takes a few seconds for large dictionaries and makes all subsequent lookups fast. The sidecar can be deleted safely at any time — it will simply be rebuilt.
On the very first lookup with a dictionary (and again whenever the `.idx` or `.syn` source file changes), the reader shows *"Indexing dictionary…"* while it builds small sidecar files next to them — a `.qidx` for the word index, and a `.sidx` when a `.syn` synonym file is present. Each sidecar is rebuilt independently, only when its own source changes. This takes a few seconds for large dictionaries and makes all subsequent lookups fast. The sidecars can be deleted safely at any time — they will simply be rebuilt.
### How Lookup Works
1. **Direct match** — the word is found as-is (case-insensitive) in the dictionary index. Surrounding punctuation is ignored.
2. **Stemming** — on a miss, common English word forms are retried automatically: possessives and plurals (`dogs` → `dog`, `stories` → `story`) and verb endings (`walked` → `walk`, `running` → `run`, `making` → `make`).
3. **Not found** — a short popup appears and you return to word selection.
2. **Synonyms** — on a miss, if the dictionary ships a `.syn` file, alternate spellings and irregular forms recorded there are resolved to their headword (e.g. `oxen` → `ox`, `colour` → `color`). This step is skipped if the `.sidx` sidecar could not be built (e.g. transient low memory during indexing); the dictionary otherwise stays usable, and the build is retried the next time it is opened.
3. **Stemming** — still no match: common English word forms are retried automatically: possessives and plurals (`dogs` → `dog`, `stories` → `story`) and verb endings (`walked` → `walk`, `running` → `run`, `making` → `make`).
4. **Not found** — a short popup appears and you return to word selection.
## The Definition Screen
+10 -3
View File
@@ -90,12 +90,19 @@ if (parsedSize != fileSize) {
## `section.bin`
### Version 35
### Version 37
Each file in `sections/*.bin` stores one laid-out spine section. The header is
also the cache-busting key: if any layout-affecting setting differs from the
current reader settings, the section is discarded and rebuilt.
Version 37 increases the fixed-size footnote href field from 96 to 256 bytes.
This changes each serialized footnote record from 128 to 288 bytes, so older
section caches must be discarded and rebuilt.
Version 36 invalidates cached word positions after ruby and CJK justification
layout changes.
Version 35 adds a header offset and a `uint32_t` entry per page for the
visible-text offset LUT. The other section LUTs remain unchanged.
@@ -135,10 +142,10 @@ import std.mem;
import std.string;
import std.core;
#define EXPECTED_VERSION 35
#define EXPECTED_VERSION 37
#define MAX_STRING_LENGTH 65535
#define FOOTNOTE_NUMBER_LEN 32
#define FOOTNOTE_HREF_LEN 96
#define FOOTNOTE_HREF_LEN 256
struct String {
u32 length [[hidden, comment("String byte length")]];
-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;
+28 -140
View File
@@ -99,11 +99,8 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
s.miniIntervals = nullptr;
delete[] s.miniGlyphs;
s.miniGlyphs = nullptr;
for (auto& chunk : s.miniBitmapChunks) {
delete[] chunk;
chunk = nullptr;
}
s.miniBitmapChunkCount = 0;
delete[] s.miniBitmap;
s.miniBitmap = nullptr;
s.miniIntervalCount = 0;
s.miniGlyphCount = 0;
s.miniIntervalCapacity = 0;
@@ -191,10 +188,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 +704,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,65 +1010,31 @@ 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).
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
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.
uint32_t span = 0;
uint32_t miniBitmapOffset = 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;
if (len == 0) {
glyph.dataOffset = span;
if (glyph.dataLength == 0) {
glyph.dataOffset = miniBitmapOffset;
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);
}
// 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;
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);
@@ -1093,20 +1046,18 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
}
seekCount++;
}
if (file.read(s.miniBitmapChunks[chunkIdx] + off, len) != static_cast<int>(len)) {
if (file.read(s.miniBitmap + miniBitmapOffset, glyph.dataLength) != static_cast<int>(glyph.dataLength)) {
LOG_ERR("SDCF", "Prewarm: short bitmap read (style %u)", styleIdx);
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
lastBitmapEnd = fileOff + len;
lastBitmapEnd = fileOff + glyph.dataLength;
glyph.dataOffset = span;
span += len;
glyph.dataOffset = miniBitmapOffset;
miniBitmapOffset += glyph.dataLength;
}
s.miniBitmapUsed = span; // footprint incl. padding: underuse-hysteresis signal
s.miniBitmapCapacity = s.miniBitmapChunkCount * MINI_BM_CHUNK_SIZE;
}
uint32_t sdTime = millis() - sdStart;
@@ -1129,10 +1080,7 @@ 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.
s.miniData.bitmap = nullptr;
s.miniData.bitmap = s.miniBitmap;
s.miniData.glyph = s.miniGlyphs;
s.miniData.intervals = s.miniIntervals;
s.miniData.intervalCount = s.miniIntervalCount;
@@ -1516,45 +1464,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 +1495,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 +1509,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;
@@ -1619,14 +1517,4 @@ 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; }
+3 -47
View File
@@ -94,14 +94,6 @@ 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);
@@ -113,11 +105,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,16 +129,6 @@ 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{};
@@ -211,16 +188,12 @@ 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.
uint8_t* miniBitmapChunks[MINI_BM_MAX_CHUNKS] = {};
uint32_t miniBitmapChunkCount = 0; // chunks currently allocated (0..count-1), kept across pages
uint8_t* miniBitmap = nullptr;
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 +243,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 +255,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_; };
+6 -1
View File
@@ -802,7 +802,12 @@ int Epub::getSpineItemsCount() const {
return bookMetadataCache->getSpineCount();
}
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; }
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return 0;
}
return bookMetadataCache->getCumulativeSize(spineIndex);
}
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
-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;
+19
View File
@@ -481,11 +481,30 @@ bool BookMetadataCache::load() {
serialization::readString(bookFile, coreMetadata.coverItemHref);
serialization::readString(bookFile, coreMetadata.textReferenceHref);
// Cache cumulative spine sizes in RAM. The progress bar (every render) and percent
// jumps otherwise pay 2 seeks + a heap-allocating SpineEntry read per access. Spine
// entries are stored contiguously in index order immediately after the LUTs, so read
// them in a single sequential pass.
cumulativeSizes.clear();
cumulativeSizes.reserve(spineCount);
const uint32_t lutSize = (static_cast<uint32_t>(spineCount) + tocCount) * sizeof(uint32_t);
bookFile.seek(lutOffset + lutSize);
for (uint16_t i = 0; i < spineCount; i++) {
cumulativeSizes.push_back(readSpineEntry(bookFile).cumulativeSize);
}
loaded = true;
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
return true;
}
uint32_t BookMetadataCache::getCumulativeSize(const int index) const {
if (index < 0 || index >= static_cast<int>(cumulativeSizes.size())) {
return 0;
}
return cumulativeSizes[index];
}
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
if (!loaded) {
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
+9
View File
@@ -7,6 +7,7 @@
#include <deque>
#include <memory>
#include <string>
#include <vector>
class BookMetadataCache {
public:
@@ -62,6 +63,11 @@ class BookMetadataCache {
// wrapper serves whichever pass is active (spine, then toc).
std::unique_ptr<serialization::BufferedFileWriter> passOut;
// Cumulative spine sizes, cached in RAM at load() so progress/percent lookups are
// O(1) instead of 2 seeks + a heap-allocating SpineEntry read per access (4 bytes
// per spine item; <1KB for typical books).
std::vector<uint32_t> cumulativeSizes;
// Index for fast href→spineIndex lookup (used only for large EPUBs)
struct SpineHrefIndexEntry {
uint64_t hrefHash; // FNV-1a 64-bit hash
@@ -113,6 +119,9 @@ class BookMetadataCache {
bool load();
SpineEntry getSpineEntry(int index);
TocEntry getTocEntry(int index);
// Cumulative byte size up to and including the given spine item (0 if out of range
// or not loaded). Backed by the in-RAM cumulativeSizes cache populated in load().
uint32_t getCumulativeSize(int index) const;
int getSpineCount() const { return spineCount; }
int getTocCount() const { return tocCount; }
bool isLoaded() const { return loaded; }
+4 -1
View File
@@ -3,7 +3,10 @@
#include <cstring>
#define FOOTNOTE_NUMBER_LEN 32
#define FOOTNOTE_HREF_LEN 96
#define FOOTNOTE_HREF_LEN 256
// ponytail: bumped from 96 to 256; calibre-generated EPUBs with long filenames
// and URL-encoded characters routinely exceed 96 chars (e.g.
// "Author-Title_split_NNN.html#_ftnN" encoded is ~150 chars).
struct FootnoteEntry {
char number[FOOTNOTE_NUMBER_LEN];
-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
+6 -1
View File
@@ -31,7 +31,12 @@ 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;
// v36: Ruby and CJK justification layout changes invalidate cached word positions.
// v37: Footnote href records grew from 96 to 256 bytes.
// v38: Image top margin is clamped so a full-viewport-height image cannot
// overflow the page bottom; older caches can hold placements that panels
// with no bottom inset refuse to draw.
constexpr uint8_t SECTION_FILE_VERSION = 38;
// 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
Loaded 20 of 114 files, more files were not shown because too many files have changed in this diff. Show more