mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf9fea5923 | ||
|
|
e9c3a166f5 | ||
|
|
d720891b2f | ||
|
|
e4aae7baef | ||
|
|
7f8b005ae1 | ||
|
|
81028d5849 | ||
|
|
0423276d2c | ||
|
|
596f0d3f71 | ||
|
|
6af9a04934 | ||
|
|
1582e70ef4 | ||
|
|
d4733bfc65 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -31,3 +31,4 @@ lib/EpdFont/scripts/output/
|
||||
/dependencies.lock
|
||||
/sdkconfig.*
|
||||
sdkconfig.sticky
|
||||
flake.*
|
||||
+179
-68
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
/home/urit/Documents/CrossPoint/crosspoint-reader/.skills/SKILL.md
|
||||
+6
-4
@@ -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
@@ -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")]];
|
||||
|
||||
+1
-1
Submodule freeink-sdk updated: 8b8337b586...a485dc46ef.
@@ -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
@@ -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; }
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in new issue
Block a user