Compare commits

..
Author SHA1 Message Date
Justin Mitchell c3c2d7b099 Merge remote-tracking branch 'origin/develop' into feat-bluetooth 2026-08-07 05:38:14 -04:00
Justin Mitchell bcb8a74fad Add weak fallback for _btLibraryInUse symbol
Provides a weak definition of _btLibraryInUse to fix linker errors when using BLE controller-only builds with NimBLE-Arduino host. The arduino-esp32 core references this symbol but only defines it when IDF host stacks are enabled. This shim ensures the symbol is always available without conflicts.
2026-08-07 05:32:34 -04:00
Justin Mitchell 2c8bd8ea1a Fix heap check to exclude framebuffer from threshold
The framebuffer loan provides a large contiguous block for inflate but cannot satisfy small allocations during CSS parsing and layout. Including it in the heap check made the threshold effectively always-false (48KB > 40KB floor), allowing builds to run at ~3KB free heap and crash in sscanf when BLE is connected. Now checks only general heap so BLE recovery runs before the build instead of crashing during it.
2026-08-07 05:23:37 -04:00
Justin Mitchell dd2ec5b525 Update freeink-sdk submodule
Update freeink-sdk submodule from a485dc4 to 8b8337b.
2026-08-07 05:20:59 -04:00
Uri Tauber e00f5958df feat: guard against cross-chip firmware installs (#2880) 2026-08-07 09:42:29 +03:00
lranslot 255bab311a fix: read platformio.ini as UTF-8 in git_branch.py (#2897) 2026-08-07 02:05:45 +03:00
Justin Mitchell c507e54478 fix: thread-safety issues in credential store (#2834) 2026-08-07 01:16:22 +03:00
Uri Tauber 5eec70de36 add IPA intervals to SD fonts 2026-08-07 01:08:29 +03:00
+19 28f3260c51 chore: update translations (#2740)
Co-authored-by: Leopoldo Pla Sempere <leopoldo.pla@treelogic.com>
Co-authored-by: muhas <muhas@muhas.ru>
Co-authored-by: Matteo Scopel <matteo@scopel.email>
Co-authored-by: CookieCaptainD <65847023+CookieCaptainD@users.noreply.github.com>
Co-authored-by: Poc031205 <165477165+Poc031205@users.noreply.github.com>
Co-authored-by: Rodrigo Matsuura <matsuura@outlook.com.br>
Co-authored-by: Spigaw <73850535+Spigaw@users.noreply.github.com>
Co-authored-by: Tom-Inge Larsen <tom.inge@gmail.com>
Co-authored-by: Baris Albayrak <80099286+barbarhan@users.noreply.github.com>
Co-authored-by: Barış Albayrak <barisa@pop-os.lan>
Co-authored-by: Hendra Kusumah <h.kusumah87@gmail.com>
Co-authored-by: thehijacker <thehijacker@gmail.com>
Co-authored-by: Andrej Kralj <andrej.kralj@gmail.com>
Co-authored-by: Bastian <391732+dasrecht@users.noreply.github.com>
Co-authored-by: th0m4sek <53303628+th0m4sek@users.noreply.github.com>
Co-authored-by: Dexif <spev@gurtam.team>
Co-authored-by: Mirus <mirusim@gmail.com>
Co-authored-by: Mirza Arnaut <mirza.arnaut45@gmail.com>
Co-authored-by: Husam Younis <youhusam@gmail.com>
Co-authored-by: Ing. Jan Kaláb <jan+github@kalabovi.org>
Co-authored-by: Tadas <tadasss@gmail.com>
Co-authored-by: winst0niuss <chumachenko.vadym@gmail.com>
Co-authored-by: fsocietyipa <fsociety.ipa@gmail.com>
Co-authored-by: Okzhetpes <okzhetpes.valikhanov@gmail.com>
Co-authored-by: KymAndriy <32357131+KymAndriy@users.noreply.github.com>
Co-authored-by: KymAndriy <test@notamail.ua>
Co-authored-by: Martin <35097765+neresus@users.noreply.github.com>
Co-authored-by: Hoang Manh Linh <jetaudio.media@gmail.com>
2026-08-06 23:39:05 +03:00
Phạm Bình An 046827fc31 fix(epub): ruby overflows right border when justifying text, unwanted short lines in Chinese and Japanese (#2781) 2026-08-06 23:26:09 +03:00
Justin Mitchell 28af4189d4 Chunk font bitmap allocations to avoid fragmentation
Replace single contiguous miniBitmap allocation with fixed 4KB chunks to allow large font page bitmaps (20-40KB) to fit in fragmented heap. Each glyph is kept whole within one chunk by padding to next chunk boundary when needed. Falls back to overflow path if glyph exceeds chunk size or max chunks reached. Mirrors existing image .pxc slot chunking strategy.
2026-08-05 16:21:17 -04:00
Justin Mitchell f45f75f966 Add emergency buffer for font glyph rendering
Prevents bold-italic glyphs from rendering blank under heap pressure (when BLE is resident). Pre-allocates a 1KB emergency buffer at font load time when heap is healthy. When per-glyph malloc fails during overflow, falls back to this buffer to render the glyph directly instead of returning null. The emergency buffer bypasses the cache ring and is overwritten on each use, which is safe since getGlyphBitmap() consumes bitmaps immediately.
2026-08-05 15:28:48 -04:00
Justin Mitchell 4564c3e743 Make BT memory patch script robust to package location
Replace hardcoded path lookup with a multi-strategy search that tries PlatformIO's package API first, then falls back to globbing common package directories. This handles cases where the framework package name or location varies across installations.
2026-08-05 15:18:44 -04:00
Justin Mitchell 1373bef1c6 Fix undefined _btLibraryInUse in BLE-controller builds
Add pre-build script to patch arduino-esp32 core header, converting extern declaration to weak definition. This resolves linker errors when using NimBLE-Arduino host with ESP-IDF BLE controller only (no IDF host stack). The weak definition allows NimBLE translation units to provide the symbol while still allowing the core's strong definition to take precedence when an IDF host stack is enabled.
2026-08-05 12:57:06 -04:00
Justin Mitchell 7dbfcbb52d Split font scan buffers by text style
Previously a single scan buffer was used for all text styles (regular, bold, italic, bold-italic), causing the font decompressor to allocate full-page buffers for each style during prewarming. This led to OOM failures when the BLE stack was resident, causing bold/italic glyphs to render blank.

Now each style maintains its own scan buffer, sized only for the text actually rendered in that style. This keeps secondary style buffers small and prevents allocation failures.
2026-08-05 12:35:49 -04:00
Justin Mitchell 981e73c137 Merge remote-tracking branch 'origin/develop' into feat-bluetooth 2026-08-05 10:59:48 -04:00
Justin Mitchell 200abc6aa4 Replace vector with deque for ParsedText tokens
Prevents firmware crashes on ESP32-C3 when parsing CJK text with thousands of tokens. Vector's contiguous reallocation (64-128 KB) fails under fragmented heap with BLE active. Deque grows in ~512 B chunks, keeping max contiguous allocation under 2 KB regardless of token count.
2026-07-31 01:09:24 -04:00
Justin Mitchell f59f72fed3 Fix BLE controller build for NimBLE-Arduino host
Disable unused BLE Mesh component which fails to compile in controller-only builds, and provide weak _btLibraryInUse symbol to retain BT controller memory when using NimBLE-Arduino host with IDF host stacks disabled.
2026-07-31 00:56:26 -04:00
Justin Mitchell f3a4b98711 Update freeink-sdk submodule
Update freeink-sdk submodule from e514a86 to fa4d5ac.
2026-07-29 14:59:01 -04:00
Justin Mitchell 4af4a6ad44 Remove BLE library ignore to fix NimBLE headers
The pioarduino core builder maps the BLE Arduino library to ESP-IDF's bt component and strips headers like esp_bt.h that NimBLE-Arduino requires. Removing lib_ignore prevents this stripping and allows NimBLE to access necessary headers.
2026-07-29 14:58:15 -04:00
Justin Mitchell d656fec87a Merge remote-tracking branch 'origin/develop' into feat-bluetooth 2026-07-29 14:37:02 -04:00
Justin Mitchell 9b16a8e263 Skip grayscale re-render for non-cached images
Avoid re-streaming large images from SD card during grayscale passes when they don't fit in RAM cache. Images that fit in the slot keep 4-level antialiasing, while oversized ones stay black-and-white from the initial pass. This optimization reduces full-page image render time from ~7.8s by preventing up to 13 redundant file reads.
2026-07-24 12:03:47 -04:00
Justin Mitchell f6566bcc74 Merge remote-tracking branch 'origin/develop' into feat-bluetooth
# Conflicts:
#	.gitignore
#	freeink-sdk
#	lib/EpdFont/SdCardFont.cpp
#	lib/EpdFont/SdCardFont.h
#	lib/GfxRenderer/GfxRenderer.cpp
#	platformio.ini
#	src/JsonSettingsIO.cpp
#	src/MappedInputManager.cpp
#	src/MappedInputManager.h
#	src/activities/Activity.h
#	src/activities/reader/EpubReaderActivity.cpp
#	src/activities/reader/EpubReaderActivity.h
#	src/activities/reader/EpubReaderMenuActivity.h
#	src/activities/reader/ReaderUtils.h
#	src/activities/settings/OtaUpdateActivity.cpp
#	src/activities/settings/SettingsActivity.cpp
#	src/activities/settings/SettingsActivity.h
#	src/main.cpp
#	src/network/CrossPointWebServer.cpp
2026-07-24 01:04:17 -04:00
Justin Mitchell a86a644adc Reduce TLS memory requirements for KOReader sync
Split heap gate into separate free-memory (50KB) and largest-block (20KB) thresholds based on field measurements. Enable wolfSSL single-precision ECC to use fixed 256-bit arrays instead of heap-allocated fast-math bignums, reducing TLS handshake memory footprint. Reclaim ~7KB by right-sizing ESP timer task stacks and move WiFi code out of IRAM to free ~25-30KB for heap. Add memory audit landmarks for font and EPUB allocations.
2026-07-14 14:53:41 -04:00
Justin Mitchell 02398c8a96 Add memory profiling for EPUB and heap diagnostics
Adds residentBytes() and cssRuleCount() methods to track EPUB memory usage including parsed CSS. Enhances memory audit logging with EPUB heap usage, max allocatable block size, and CSS rule count. Implements one-shot heap block map dump on first settled audit to identify fragmentation and long-lived allocations.
2026-07-14 13:21:15 -04:00
Justin Mitchell ba92cb902e Add heap memory reporting for fonts and sections
Implements reportMemory() methods across SdCardFont, SdCardFontManager, and Section classes to track resident heap usage. Adds heap audit logging in EpubReaderActivity to attribute memory to fonts, framebuffer, section data, and other components for measurement-driven optimization.
2026-07-14 12:08:29 -04:00
Justin Mitchell 4d19d38152 Add idle-time glyph prewarming for next page
Preload glyphs for the next page during idle time to reduce page-turn latency. After a 400ms debounce period, scan the next page in FCM scan mode (no pixels drawn) to cache missing glyphs from SD card, avoiding ~100ms of SD reads during the actual page turn. Prewarm is deferred when rendering is active, heap is low, or during rapid page-flipping.
2026-07-14 11:26:07 -04:00
Justin Mitchell 302c0771dd Batch file list responses to reduce TCP segments
Buffer JSON file list entries into ~1.4KB batches before sending to avoid one TCP segment per file. This eliminates client delayed ACK delays that were causing large directory listings to take tens of seconds. Falls back to per-entry sends if buffer allocation fails.
2026-07-14 05:15:00 -04:00
Justin Mitchell c1a396c1ba Stop BLE before initializing WiFi in activities
Explicitly stop the BLE stack before bringing up WiFi in CrossPointWebServerActivity, FontDownloadActivity, and OtaUpdateActivity. On ESP32-C3, the shared radio and heap between BLE and WiFi stacks causes WiFi to be permanently starved if initialized while NimBLE's ~50KB is still resident, as the WiFi driver sizes its RX/TX buffer pools at init time.
2026-07-14 05:04:22 -04:00
Justin Mitchell 078d9ef535 Update status bar immediately on BLE connect/disconnect
Previously the status bar showed 'BT connecting' until the next page turn. Now monitors connection state in loop() and redraws the status bar immediately when connection completes or disconnects, restoring the chapter/book title. The connecting message now takes over the entire title slot instead of prepending to it.
2026-07-14 04:47:48 -04:00
Justin Mitchell 17e4230068 Add async display refresh to overlap CPU work
Introduces non-blocking display refresh methods that allow CPU work (like grayscale rendering) to overlap with the e-ink panel's refresh time. The async path starts the waveform and returns immediately, with the caller responsible for waiting via waitRefreshComplete(). Falls back to blocking refresh when fadingFix is enabled or the panel lacks deferral support.
2026-07-14 04:42:27 -04:00
Justin Mitchell 34e7ed249b Merge remote-tracking branch 'origin/develop' into feat-bluetooth 2026-07-14 04:20:50 -04:00
Justin Mitchell cecdbefa0e Normalize whitespace in comments and includes
Standardize spacing in inline comments and remove extra blank line in include statement for consistency
2026-07-09 00:21:25 -04:00
Justin Mitchell 4c5fd653c0 Add Bluetooth status icon to reader status bar
Display a Bluetooth icon in the status bar when BLE is connected. Also optimize memory management by lending the framebuffer instead of immediately tearing down BLE when heap is low, allowing BLE to remain active during rendering.
2026-07-09 00:13:54 -04:00
Justin Mitchell 05c1e9aa46 Add framebuffer lending for memory-intensive builds
Temporarily release framebuffer memory during section pagination to reduce heap pressure. The framebuffer can be released before memory-intensive operations and restored afterward, allowing BLE stack startup threshold to be lowered from 80KB to 70KB. Implements RAII-style FrameBufferBuildLoan class to ensure proper cleanup and automatic restart on restoration failure.
2026-07-09 00:04:11 -04:00
Justin Mitchell f3da5e4f06 Merge branch 'develop' of https://github.com/crosspoint-reader/crosspoint-reader into feat-bluetooth 2026-07-08 22:13:16 -04:00
Justin Mitchell 6f0707e83c Suppress cppcheck warnings for CI compatibility
Add missingInclude suppression to prevent false positives on fresh CI checkouts where include paths are unresolved. Add inline suppression for constVariableReference warning where cppcheck cannot detect mutations without full include path resolution.
2026-07-06 08:55:24 -04:00
Justin Mitchell 7b6df60a54 Shrink NimBLE footprint to prevent stack overflow
Reduce BLE stack memory usage from ~68KB to ~52KB by disabling unused NimBLE roles (peripheral, broadcaster), limiting connections to 1, and reducing buffer counts for ACL, HCI events, and ATT entries. Remove unused cloud components (esp_insights, esp_rainmaker) that require unavailable server certificates. These changes prevent stack collision with the render shed, eliminating restart flaps on the reader device.
2026-07-06 08:48:44 -04:00
Justin Mitchell 1c13913713 Implement keep-if-fits buffer reuse to reduce heap fragmentation
Replace per-page buffer free+realloc pattern with capacity tracking that only reallocates when needed size exceeds current capacity. This prevents heap fragmentation from non-coalescing holes that occurred when each page's freed block rarely fit the next page's allocation. After a few page turns, capacities converge on the book's maximum and page turns stop touching the allocator entirely.
2026-07-06 08:30:17 -04:00
Justin Mitchell 613371b716 Add heap pressure management for BLE and rendering
Prevent OOM aborts by shedding BLE stack when free heap drops below 24KB before rendering. Lower BLE start threshold from 100KB to 80KB to match steady-state heap levels (~84KB). Reserve vector capacity upfront in page parsing to avoid reallocation crashes on fragmented heaps. Gate background section builds on heap availability.
2026-07-06 08:12:11 -04:00
Justin Mitchell 0d7a84e3c5 Rename variable count to scanCount for clarity
Improves code readability by using a more descriptive variable name that better indicates the variable holds the device count from a BLE scan.
2026-07-06 02:23:04 -04:00
Justin Mitchell 4c93950771 Merge branch 'pr-2527' into feat-bluetooth
# Conflicts:
#	src/activities/reader/EpubReaderActivity.cpp
2026-07-06 02:10:55 -04:00
Justin Mitchell 1d01eebf55 Reformat Bluetooth scan log statement 2026-07-06 02:06:25 -04:00
Justin Mitchell 9bf6e5f43c Merge remote-tracking branch 'origin/develop' into feat-bluetooth
# Conflicts:
#	.gitmodules
#	freeink-sdk
#	platformio.ini
#	src/activities/reader/EpubReaderActivity.cpp
2026-07-06 02:04:38 -04:00
Nick ad3138983b fix: clear the BT-paused popup with an immediate redraw (toast semantics)
The popup lingered until the next page turn. E-ink has no free timers --
clearing costs a refresh whenever it happens -- so request the redraw
right away: the popup shows for the ~2 s page re-render, then clears.
2026-07-02 17:25:34 -05:00
Nick 793685e76b feat: user path to resume BT from low-memory pause via reader menu toggle
Field session: after a recovery teardown the heap settles at ~72 KB --
below the 100 KB start floor -- and never recovers on its own (the
defrag silent-restart only fires when a build FAILS, and builds succeed
now). BT stayed paused forever, the menu toggle claimed ON, and toggling
off/on changed nothing.

- Menu label tells the truth: ON / PAUSED (enabled but stack down) / OFF
  (new STR_STATE_PAUSED string)
- Toggling BT on below the heap floor silent-restarts into the current
  book: the fresh boot's ~118 KB passes the gate and BT auto-starts on
  resume. Explicit user intent is the right trigger for the defrag.
- Hoist the floor to bleinput::kStartMinFreeHeap, shared by the
  lifecycle gate and the toggle
2026-07-02 17:25:34 -05:00
Nick 551d29744a fix: stop BLE lifecycle oscillation after build recovery
The 70 KB restart floor was arithmetic nonsense: NimBLE takes ~57 KB, so
a restart at 70 KB free left ~13 KB -- below the 40 KB build pre-flight
-- so the next chapter build instantly re-entered recovery and tore BLE
back down. Field symptom: endless 'BT Connecting...' popup + redraw loop.

- Single conservative floor: 100 KB (57 KB stack + 40 KB build headroom)
- 30 s cool-down after any recovery teardown before the lifecycle may
  restart BLE, so a marginal heap can never flap; the 'BT paused (low
  memory)' popup shows instead and reading continues without the remote
2026-07-02 17:25:10 -05:00
Nick 74a0969cc6 fix: pre-flight heap floor before section builds; no inline BLE restart
Layout code (line-break DP arrays, CSS lookups, glyph buffers) allocates
via std::vector/std::string and abort()s on OOM under -fno-exceptions --
it cannot fail cleanly mid-build. Field crashes (X4, BLE resident):
builds entered at ~11 KB free and aborted in ParsedText::
computeLineBreaks; an inline BLE restart after recovery re-starved the
render and abort()ed in FontCacheManager's scanText_.reserve at ~8 KB.

- BUILD_MIN_FREE_HEAP (40 KB): pre-flight before the build; below the
  floor go straight to recovery instead of attempting a doomed build
- Recovery no longer restarts NimBLE inline; the lifecycle stays paused
  until the render completes (scoped unpause guard), then the main-loop
  lifecycle restarts BLE behind its own heap gate
2026-07-02 17:24:29 -05:00
Nick 06aa1ac2f7 feat: silent-restart defrag on section-build double failure + BLE debug flags
- EpubReaderActivity: when a section build fails even after the BLE
  teardown/retry, silentRestartToReader() as last-resort heap defrag,
  guarded by bootWasSilentRestart() to prevent reboot loops
- main/SilentRestart.h: expose bootWasSilentRestart()
- platformio.ini: enable FREEINK_BLE_HID_REPORT_DEBUG in env:default
2026-07-02 17:20:51 -05:00
Justin Mitchell c463da994e Add BLE lifecycle pause mechanism for memory mgmt
Introduces a global pause flag to temporarily block BLE stack auto-start during large memory allocations (e.g., in EPUB rendering). Also adds auto-restart logic for BLE scanning when device list is empty, and extensive debug logging for BLE scan lifecycle troubleshooting.
2026-06-30 16:31:54 -04:00
Justin Mitchell 988652513a Add FreeInk dependencies
Include BoardConfig, PowerManager, Rtc, Imu, FreeInkUI, and Icons libraries from the FreeInk SDK to support hardware abstraction layer functionality.
2026-06-24 17:41:36 -04:00
Justin Mitchell 28d0d6f5d2 Replace range-based for loop with std::fill
Use standard algorithm std::fill instead of manual loop to clear bleKeyMap array. This is more idiomatic C++ and expresses intent more clearly.
2026-06-24 17:15:44 -04:00
Justin Mitchell 568831f232 Update freeink-sdk submodule
Update freeink-sdk submodule from b026965 to a913bb3.
2026-06-24 17:10:58 -04:00
Justin Mitchell d6f5be6b7a Add keepsBluetoothAlive hook to Activity base class
Introduces a virtual method allowing activities to indicate whether they need the BLE stack to remain active. This enables selective teardown of Bluetooth to free heap memory, with the Bluetooth settings screen being able to override this for pairing and scanning functionality.
2026-06-24 17:00:52 -04:00
Justin Mitchell d30fde2f4d Add Bluetooth connecting popup message
Add new translation string for displaying a popup message while Bluetooth connection is in progress.
2026-06-24 16:21:49 -04:00
Justin Mitchell 6305777b22 Retry EPUB section build after freeing BLE stack
When building an EPUB section fails with Bluetooth enabled, temporarily stop the BLE stack to free up memory (~16KB), retry the build, then restart BLE. This works around memory fragmentation caused by the NimBLE stack that prevents allocating the large contiguous buffer needed for inflate/deflate operations. The recovery only runs once per uncached chapter since chapters are cached afterwards.
2026-06-24 16:13:43 -04:00
Justin Mitchell 9ab0b0bfb7 Add Bluetooth HID remote control support
Integrate BLE keyboard host functionality for page-turner remotes. Adds device pairing, button mapping, preset configurations for Free2/Free3 remotes, and persistent storage of mappings. Migrates from open-x4-sdk to freeink-sdk submodule which includes the BleKeyboardHost library. Implements CPU frequency locking during BLE operations to prevent watchdog timeouts.
2026-06-24 15:36:29 -04:00
103 changed files with 4162 additions and 718 deletions

No files matched your search

+1 -2
View File
@@ -29,6 +29,5 @@ lib/EpdFont/scripts/output/
/.dummy
/CMakeLists.txt
/dependencies.lock
/sdkconfig.default
/sdkconfig.defaults
/sdkconfig.*
sdkconfig.sticky
+1
View File
@@ -153,6 +153,7 @@ To convert your own TrueType/OpenType fonts:
| `armenian` | Armenian |
| `ethiopic` | Ethiopic + Extended |
| `vietnamese` | Vietnamese subset (ơ/ư and combining marks) |
| `ipa-chars` | IPA Extensions + Spacing Modifier Letters (phonetic transcription) |
| `punctuation` | General punctuation (U+2000–U+206F) |
| `cjk` | CJK Unified Ideographs + Hiragana + Katakana + Fullwidth |
| `hangul` | Korean Hangul syllables + Jamo + Compatibility Jamo |
+5
View File
@@ -369,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;
+83 -26
View File
@@ -191,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]);
}
@@ -707,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_);
@@ -1013,20 +1023,21 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
totalBitmapSize += s.miniGlyphs[i].dataLength;
}
// 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; });
// Assemble the bitmap arena from on-demand 4 KB chunks (see MINI_BM_CHUNK_*).
// `span` is the running virtual offset into the arena; each glyph is placed
// wholly within one chunk, skipping past a boundary rather than straddling it.
// glyph.dataOffset becomes the virtual offset, decoded at render time by
// miniGlyphBitmap(). Chunks are allocated only as span reaches them, so a
// sparse page never allocates the full ceiling.
// 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;
@@ -1035,17 +1046,16 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
continue;
}
if (len > MINI_BM_CHUNK_SIZE) {
// A single glyph larger than one chunk cannot be placed. Real fonts never
// hit this (a 4 KB 2bpp glyph would be enormous); bail rather than corrupt.
// 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);
file.close();
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
// Skip to the next chunk boundary if this glyph would straddle one.
// 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;
@@ -1053,7 +1063,6 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
const uint32_t chunkIdx = span >> MINI_BM_CHUNK_SHIFT;
if (chunkIdx >= MINI_BM_MAX_CHUNKS) {
LOG_ERR("SDCF", "Prewarm: mini bitmap needs > %u chunks (style %u)", MINI_BM_MAX_CHUNKS, styleIdx);
file.close();
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
@@ -1063,7 +1072,6 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
s.miniBitmapChunks[chunkIdx] = new (std::nothrow) uint8_t[MINI_BM_CHUNK_SIZE];
if (!s.miniBitmapChunks[chunkIdx]) {
LOG_ERR("SDCF", "Failed to allocate mini bitmap chunk %u (style %u)", chunkIdx, styleIdx);
file.close();
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
@@ -1073,7 +1081,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
}
const uint32_t off = span & (MINI_BM_CHUNK_SIZE - 1);
uint32_t fileOff = s.bitmapFileOffset + glyph.dataOffset;
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);
@@ -1097,8 +1105,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
glyph.dataOffset = span;
span += len;
}
// Footprint including inter-chunk padding: the underuse-hysteresis signal.
s.miniBitmapUsed = span;
s.miniBitmapUsed = span; // footprint incl. padding: underuse-hysteresis signal
s.miniBitmapCapacity = s.miniBitmapChunkCount * MINI_BM_CHUNK_SIZE;
}
@@ -1122,9 +1129,9 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
s.miniMetadataOnly = metadataOnly;
s.miniHysteresisPending = !metadataOnly; // one hysteresis evaluation per rebuild
memset(&s.miniData, 0, sizeof(s.miniData));
// The mini bitmap is chunked (non-contiguous), so there is no single base
// pointer. Prewarmed SD glyphs are resolved via SdCardFont::miniGlyphBitmap()
// in the renderer; leave bitmap null so any stray &bitmap[dataOffset] faults.
// 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;
@@ -1509,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;
@@ -1540,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;
}
@@ -1554,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;
+41 -18
View File
@@ -94,10 +94,12 @@ class SdCardFont {
// Returns the bitmap for an on-demand-loaded (overflow) glyph.
const uint8_t* getOverflowBitmap(const EpdGlyph* glyph) const;
// Resolves a prewarmed mini glyph's chunked bitmap. `ctx` is the glyphMissCtx
// (an OverflowContext identifying the style); `dataOffset` is the glyph's
// virtual offset into the style's chunked arena. Returns nullptr if the chunk
// is absent or out of range. Called by GfxRenderer::getGlyphBitmap().
// 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.
@@ -111,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_; }
@@ -135,17 +142,15 @@ class SdCardFont {
uint8_t ligaturePairCount = 0;
};
// The per-style mini bitmap arena is stored as a list of fixed-size chunks
// rather than one contiguous block. A whole page's 2bpp glyph bitmaps can run
// tens of KB; on a fragmented heap a single contiguous allocation of that size
// fails even when the same bytes are available as several smaller free blocks.
// Chunking lets the arena be assembled from blocks the allocator can actually
// provide. Each glyph's bitmap is placed wholly within one chunk (never
// straddling a boundary), so a glyph is addressed by a virtual offset that
// maps to (chunk index, offset-in-chunk) via miniGlyphBitmap().
// 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 ceiling per style/page
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 {
@@ -206,15 +211,16 @@ class SdCardFont {
EpdFontData miniData{};
EpdUnicodeInterval* miniIntervals = nullptr;
EpdGlyph* miniGlyphs = nullptr;
// Chunked mini bitmap arena (see MINI_BM_CHUNK_* above). Chunks are allocated
// on demand during prewarm; miniBitmapChunkCount is how many are live.
// 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;
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.
@@ -264,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;
@@ -276,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 {
+8
View File
@@ -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;
+4
View File
@@ -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_; };
@@ -49,6 +49,7 @@ INTERVAL_PRESETS = {
"armenian": [(0x0530, 0x058F)],
"ethiopic": [(0x1200, 0x137F), (0x1380, 0x139F), (0x2D80, 0x2DDF)],
"vietnamese": [(0x01A0, 0x01B0), (0x1EA0, 0x1EF9)],
"ipa-chars": [(0x0250, 0x02AF), (0x02B0, 0x02FF)],
"punctuation": [(0x2000, 0x206F)],
"cjk": [(0x3000, 0x303F), (0x3040, 0x309F), (0x30A0, 0x30FF),
(0x4E00, 0x9FFF), (0xF900, 0xFAFF), (0xFF00, 0xFFEF)],
+21 -21
View File
@@ -35,7 +35,7 @@ families:
- name: Literata
description: "Screen-optimized serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/googlefonts/literata/main/fonts/ttf/Literata-Regular.ttf"}
@@ -45,7 +45,7 @@ families:
- name: SourceSerif4
description: "Adobe transitional serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-serif/release/TTF/SourceSerif4-Regular.ttf"}
@@ -55,7 +55,7 @@ families:
- name: NotoSerifExtended
description: "Serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/notofonts/NotoSerif/main/fonts/ttf/unhinted/instance_ttf/NotoSerif-Regular.ttf"}
@@ -65,7 +65,7 @@ families:
- name: Merriweather
description: "Warm serif for long-form reading (Latin, Cyrillic)"
intervals: latin-ext,cyrillic
intervals: latin-ext,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/SorkinType/Merriweather/master/fonts/ttf/Merriweather-Regular.ttf"}
@@ -75,7 +75,7 @@ families:
- name: Lora
description: "Calligraphic serif for literary reading (Latin, Cyrillic)"
intervals: latin-ext,cyrillic
intervals: latin-ext,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/main/fonts/ttf/Lora-Regular.ttf"}
@@ -85,7 +85,7 @@ families:
- name: GentiumBookPlus
description: "Scholarly serif with wide Unicode coverage (Latin, Greek, Cyrillic, IPA)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/gentiumbookplus/GentiumBookPlus-Regular.ttf"}
@@ -95,7 +95,7 @@ families:
- name: IBMPlexSerif
description: "Professional serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexserif/IBMPlexSerif-Regular.ttf"}
@@ -105,7 +105,7 @@ families:
- name: Bitter
description: "Slab serif designed for e-ink (Latin, Cyrillic)"
intervals: latin-ext,cyrillic
intervals: latin-ext,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter%5Bwght%5D.ttf", variable: {wght: 500}}
@@ -115,7 +115,7 @@ families:
- name: Domitian
description: "A humanist serif for literary reading (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://mirrors.ctan.org/fonts/domitian/opentype/Domitian-Roman.otf"}
@@ -125,7 +125,7 @@ families:
- name: LibreBaskerville
description: "A serif reimplementing the classic Baskerville (Latin)"
intervals: latin-ext
intervals: latin-ext,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Regular.ttf"}
@@ -135,7 +135,7 @@ families:
- name: Vollkorn
description: "A serif for bread and butter use by Friedrich Althausen (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 400}}
@@ -147,7 +147,7 @@ families:
- name: NotoSansExtended
description: "Sans-serif (Latin, Greek, Cyrillic, Georgian, Armenian, Ethiopic)"
intervals: latin-ext,greek,cyrillic,georgian,armenian,ethiopic
intervals: latin-ext,greek,cyrillic,georgian,armenian,ethiopic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {path: "builtinFonts/source/NotoSans/NotoSans-Regular.ttf"}
@@ -157,7 +157,7 @@ families:
- name: Inter
description: "Modern sans-serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/inter/Inter%5Bopsz%2Cwght%5D.ttf", variable: {wght: 400, opsz: 14}}
@@ -167,7 +167,7 @@ families:
- name: SourceSans3
description: "Adobe humanist sans-serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-sans/release/TTF/SourceSans3-Regular.ttf"}
@@ -177,7 +177,7 @@ families:
- name: IBMPlexSans
description: "IBM corporate sans-serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/IBM/plex/master/packages/plex-sans/fonts/complete/ttf/IBMPlexSans-Regular.ttf"}
@@ -187,7 +187,7 @@ families:
- name: Alegreya
description: "Calligraphic serif/display (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/alegreya/Alegreya%5Bwght%5D.ttf", variable: {wght: 400}}
@@ -199,7 +199,7 @@ families:
- name: IBMPlexMono
description: "Monospace for code and technical reading (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
intervals: latin-ext,greek,cyrillic,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexmono/IBMPlexMono-Regular.ttf"}
@@ -209,7 +209,7 @@ families:
- name: SourceCodePro
description: "Adobe monospace with excellent hinting (Latin)"
intervals: latin-ext
intervals: latin-ext,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-code-pro/release/TTF/SourceCodePro-Regular.ttf"}
@@ -221,7 +221,7 @@ families:
- name: OpenDyslexic
description: "Dyslexia-friendly font (Latin)"
intervals: latin-ext
intervals: latin-ext,ipa-chars
sizes: [8, 10, 12, 14]
styles:
regular: {path: "builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf"}
@@ -233,7 +233,7 @@ families:
- name: AtkinsonHyperlegibleNext
description: "Accessibility font for low vision (Latin)"
intervals: latin-ext
intervals: latin-ext,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/googlefonts/atkinson-hyperlegible-next/main/fonts/ttf/AtkinsonHyperlegibleNext-Regular.ttf"}
@@ -243,7 +243,7 @@ families:
- name: LexicaUltralegible
description: "Accessibility font for low vision / dyslexia (Latin)"
intervals: latin-ext
intervals: latin-ext,ipa-chars
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/jacobxperez/lexica-ultralegible/main/fonts/ttf/LexicaUltralegible-Regular.ttf"}
+12
View File
@@ -44,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;
+4
View File
@@ -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
+93 -83
View File
@@ -681,6 +681,76 @@ static inline bool isCjkIdeograph(uint32_t cp) {
(cp >= 0x20000 && cp <= 0x3FFFF);
}
// The first word of a line may have its ruby characters wider than the word (the base text). In that case, we need to
// move the base text to the right a bit so that ruby text doesn't overflow the left border, and it is still centered
// over the base text. This function calculates how much we need to move the base text to the right.
int ParsedText::calculateRubyExtraStartOffset(const size_t wordIdx, const size_t maxWordIdx,
const GfxRenderer& renderer, const int fontId) const {
if (rubyTexts.empty() || wordIdx >= rubyTexts.size() || rubyTexts[wordIdx].empty() ||
(wordStyles[wordIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) {
return 0;
}
size_t groupWordCount = 1;
while (wordIdx + groupWordCount < maxWordIdx &&
(wordStyles[wordIdx + groupWordCount] & EpdFontFamily::RUBY_CONTINUE) != 0) {
groupWordCount++;
}
int groupActualWidth = 0;
for (size_t k = 0; k < groupWordCount; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[wordIdx + k], wordStyles[wordIdx + k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[wordIdx].c_str(), EpdFontFamily::SUP);
if (rubyWidth <= groupActualWidth) {
return 0;
}
const int leftOverlap = (rubyWidth - groupActualWidth) / 2;
// This function is only ever called for the first word of a line.
// words[wordIdx - 1], if it exists, is always the last word of the *prior* line
// and cannot absorb any left overhang on the current line.
// The full leftOverlap must therefore be reserved as a visual indent so the
// ruby text does not overflow the left margin.
return leftOverlap;
}
// The last ruby group on a line may have its ruby characters wider than the group's base text.
// The right half of that overhang protrudes past the last base character. This function returns
// the amount of right-margin space that must be reserved so the ruby does not overflow the right
// border. It mirrors calculateRubyExtraStartOffset: words[lineBreak] is on the *next* line and
// cannot absorb any of the right overhang on the current line, so the full rightOverlap is returned.
int ParsedText::calculateRubyExtraEndOffset(const size_t lineStartIdx, const size_t lineBreakIdx,
const GfxRenderer& renderer, const int fontId) const {
if (rubyTexts.empty() || lineBreakIdx == 0 || lineStartIdx >= lineBreakIdx) {
return 0;
}
// Walk backwards from the last word to find the leader of the last ruby group on the line.
size_t leaderIdx = lineBreakIdx - 1;
while (leaderIdx > lineStartIdx && (wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) {
leaderIdx--;
}
// leaderIdx must be a ruby group leader (non-empty ruby, no RUBY_CONTINUE flag).
if (leaderIdx >= rubyTexts.size() || rubyTexts[leaderIdx].empty() ||
(wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) {
return 0;
}
// Measure the group.
int groupActualWidth = 0;
for (size_t k = leaderIdx; k < lineBreakIdx; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[k], wordStyles[k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[leaderIdx].c_str(), EpdFontFamily::SUP);
if (rubyWidth <= groupActualWidth) {
return 0;
}
return (rubyWidth - groupActualWidth) / 2;
}
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
std::vector<uint16_t> wordWidths;
wordWidths.reserve(words.size());
@@ -691,9 +761,6 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
// Adjust widths for ruby groups to comply with JLReq standards
if (!rubyTexts.empty()) {
const int ascender = renderer.getFontAscenderSize(fontId);
const int maxOverhang = ascender / 2; // Maximum overhang over non-ideographic characters (Kana, punctuation, etc.)
struct RubyGroupInfo {
size_t start;
size_t count;
@@ -730,9 +797,10 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
if (g.start > 0) {
const uint32_t cpPrev = lastCodepoint(words[g.start - 1]);
if (isCjkIdeograph(cpPrev)) {
wordWidths[g.start] += g.leftOverlap;
wordWidths[g.start - 1] += g.leftOverlap;
} else {
wordWidths[g.start] += std::max(0, g.leftOverlap - maxOverhang);
const int maxLeftOverhang = wordWidths[g.start - 1] / 2;
wordWidths[g.start - 1] += std::max(0, g.leftOverlap - maxLeftOverhang);
}
}
@@ -752,7 +820,8 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
if (isCjkIdeograph(cpNext)) {
wordWidths[g.start + g.count - 1] += g.rightOverlap;
} else {
wordWidths[g.start + g.count - 1] += std::max(0, g.rightOverlap - maxOverhang);
const int maxRightOverhang = wordWidths[nextIdx] / 2;
wordWidths[g.start + g.count - 1] += std::max(0, g.rightOverlap - maxRightOverhang);
}
// Check if there is another ruby group further ahead separated only by non-ideographs
@@ -769,8 +838,10 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
gapWidth += wordWidths[k];
}
if (onlyNonIdeographsInBetween) {
const int allowedRight = std::min(g.rightOverlap, maxOverhang);
const int allowedLeft = std::min(nextG.leftOverlap, maxOverhang);
const int maxRightOverhang = wordWidths[g.start + g.count - 1] / 2;
const int maxLeftOverhang = wordWidths[nextG.start - 1] / 2;
const int allowedRight = std::min(g.rightOverlap, maxRightOverhang);
const int allowedLeft = std::min(nextG.leftOverlap, maxLeftOverhang);
const int touchOverlap = allowedRight + allowedLeft - gapWidth;
if (touchOverlap > 0) {
wordWidths[g.start + g.count - 1] += touchOverlap;
@@ -837,36 +908,7 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
}
// Calculate extraStartOffset for the first word on the line (i) (protect left margin)
int extraStartOffset = 0;
if (j == i && !rubyTexts.empty() && i < rubyTexts.size() && !rubyTexts[i].empty() &&
(wordStyles[i] & EpdFontFamily::RUBY_CONTINUE) == 0) {
int groupWordCount = 1;
while (i + groupWordCount < totalWordCount &&
(wordStyles[i + groupWordCount] & EpdFontFamily::RUBY_CONTINUE) != 0) {
groupWordCount++;
}
int groupActualWidth = 0;
for (int k = 0; k < groupWordCount; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[i + k], wordStyles[i + k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[i].c_str(), EpdFontFamily::SUP);
if (rubyWidth > groupActualWidth) {
const int leftOverlap = (rubyWidth - groupActualWidth) / 2;
const int ascender = renderer.getFontAscenderSize(fontId);
const int maxOverhang = ascender / 2;
int expansion_added = 0;
if (i > 0) {
const uint32_t cpPrev = lastCodepoint(words[i - 1]);
if (isCjkIdeograph(cpPrev)) {
expansion_added = leftOverlap;
} else {
expansion_added = std::max(0, leftOverlap - maxOverhang);
}
}
extraStartOffset = leftOverlap - expansion_added;
}
}
const int extraStartOffset = (j == i) ? calculateRubyExtraStartOffset(i, totalWordCount, renderer, fontId) : 0;
currlen += wordWidths[j] + gap + (j == i ? extraStartOffset : 0);
@@ -879,46 +921,7 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
continue;
}
// Calculate extraEndOffset if we break after word j (protect right margin)
int extraEndOffset = 0;
if (!rubyTexts.empty() && j < rubyTexts.size()) {
bool isLastOfGroup = false;
size_t leaderIdx = j;
while (true) {
if (!rubyTexts[leaderIdx].empty() && (wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) == 0) {
isLastOfGroup = true;
break;
}
if (leaderIdx == 0 || (wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) == 0) {
break;
}
leaderIdx--;
}
if (isLastOfGroup && (j + 1 == totalWordCount || (wordStyles[j + 1] & EpdFontFamily::RUBY_CONTINUE) == 0)) {
int groupActualWidth = 0;
for (size_t k = leaderIdx; k <= j; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[k], wordStyles[k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[leaderIdx].c_str(), EpdFontFamily::SUP);
if (rubyWidth > groupActualWidth) {
const int rightOverlap = (rubyWidth - groupActualWidth) / 2;
const int ascender = renderer.getFontAscenderSize(fontId);
const int maxOverhang = ascender / 2;
int expansion_added = 0;
if (j + 1 < totalWordCount) {
const uint32_t cpNext = firstCodepoint(words[j + 1]);
if (isCjkIdeograph(cpNext)) {
expansion_added = rightOverlap;
} else {
expansion_added = std::max(0, rightOverlap - maxOverhang);
}
}
extraEndOffset = rightOverlap - expansion_added;
}
}
}
const int extraEndOffset = calculateRubyExtraEndOffset(i, j + 1, renderer, fontId);
if (currlen + extraEndOffset > effectivePageWidth) {
continue; // Cannot split here as it would overflow the right margin
@@ -939,7 +942,9 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
}
}
if (cost < dp[i]) {
// Favor longer lines when line-breaking costs are equal, to avoid unnecessary short lines in Chinese and Japanese
// text.
if (cost <= dp[i]) {
dp[i] = cost;
ans[i] = j; // j is the index of the last word in this optimal line
}
@@ -1171,6 +1176,9 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
std::copy(rubyTexts.begin() + lastBreakAt, rubyTexts.begin() + lastBreakAt + copyCount, lineRubyTexts.begin());
}
const int extraStartOffset = calculateRubyExtraStartOffset(lastBreakAt, lineBreak, renderer, fontId);
const int extraEndOffset = calculateRubyExtraEndOffset(lastBreakAt, lineBreak, renderer, fontId);
std::vector<std::string> lineWords;
lineWords.reserve(lineWordCount);
std::vector<EpdFontFamily::Style> lineWordStyles;
@@ -1225,8 +1233,9 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
? CssTextAlign::Right
: blockStyle.alignment;
// For justified text, compute per-gap extra to distribute remaining space evenly
const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
// For justified text, compute per-gap extra to distribute remaining space evenly.
// extraEndOffset reserves space for any ruby group at the right edge of the line.
const int spareSpace = effectivePageWidth - extraStartOffset - extraEndOffset - lineWordWidthSum - totalNaturalGaps;
const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
? computeJustifyExtra(spareSpace, actualGapCount)
: 0;
@@ -1307,7 +1316,8 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
}
}
const int reorderedSpare = effectivePageWidth - reorderedWordWidthSum - reorderedNaturalGaps;
const int reorderedSpare =
effectivePageWidth - extraStartOffset - extraEndOffset - reorderedWordWidthSum - reorderedNaturalGaps;
const int reorderedJustifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
? computeJustifyExtra(reorderedSpare, reorderedGapCount)
: 0;
@@ -1412,7 +1422,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
}
} else {
// LTR: position words from left to right
int xpos = firstLineIndent;
int xpos = firstLineIndent + extraStartOffset;
if (effectiveAlignment == CssTextAlign::Right) {
xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
} else if (effectiveAlignment == CssTextAlign::Center) {
+3
View File
@@ -59,6 +59,9 @@ class ParsedText {
void pushVisibleOffset(uint32_t offset);
void insertVisibleOffset(size_t wordIndex, uint32_t offset);
void eraseVisibleOffsetPrefix(size_t count);
int calculateRubyExtraStartOffset(size_t wordIdx, size_t maxWordIdx, const GfxRenderer& renderer, int fontId) const;
int calculateRubyExtraEndOffset(size_t lineStartIdx, size_t lineBreakIdx, const GfxRenderer& renderer,
int fontId) const;
int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const;
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
+1 -1
View File
@@ -31,7 +31,7 @@ namespace {
// match. Keeps <br>-per-paragraph books (common CJK formatting) from
// re-adding container spacing at every paragraph.
// v35: Persist a uint32_t visible-text start offset for every page.
constexpr uint8_t SECTION_FILE_VERSION = 35;
constexpr uint8_t SECTION_FILE_VERSION = 36;
// Written into the version field while a build is in progress; patched to
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
+15
View File
@@ -72,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;
+19
View File
@@ -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();
+7 -61
View File
@@ -135,86 +135,32 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
const bool scanning = renderer.isFontCacheScanning();
const int ascender = renderer.getFontAscenderSize(fontId);
// Resolve ruby collisions left-to-right to prevent adjacent ruby texts from overlapping
// Resolve ruby positions. Layout (extractLine) has already reserved extraStartOffset on the
// left and extraEndOffset on the right, so the centered rubyX is always within the page margins.
struct RubyDrawInfo {
int x;
int width;
std::string text;
BidiUtils::BidiBaseDir baseDir;
};
// hasRuby() is an O(numWords) scan, so resolve it once here rather than per word.
// Both arrays below are only ever read when the line carries ruby, so they stay
// empty (zero allocations) for the ruby-less case, which is every line of a
// non-CJK book. Sized lazily inside the branch.
const bool blockHasRuby = hasRuby();
std::vector<int> wordShiftArr;
std::vector<RubyDrawInfo> rubies;
if (blockHasRuby) {
wordShiftArr.assign(numWords, 0);
rubies.resize(numWords);
int accumulatedShift = 0;
int lastEnd = -9999;
for (uint16_t i = 0; i < numWords; i++) {
wordShiftArr[i] = accumulatedShift;
if (i < rubyTexts.size() && !rubyTexts[i].empty() && (wordStyle(i) & EpdFontFamily::RUBY_CONTINUE) == 0) {
// Find the group size (how many words are part of this ruby annotation)
int groupWordCount = 1;
while (i + groupWordCount < numWords && (wordStyle(i + groupWordCount) & EpdFontFamily::RUBY_CONTINUE) != 0) {
groupWordCount++;
}
// Compute actual width for the group
int groupActualWidth = 0;
for (int k = 0; k < groupWordCount; ++k) {
groupActualWidth += renderer.getTextAdvanceX(fontId, wordText(i + k), wordStyle(i + k));
}
const char* word = wordText(i);
const int leaderWordX = xposArr[i] + x;
const int leaderWordX_shifted = leaderWordX + accumulatedShift;
const auto baseDir =
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(word, blockStyle.isRtl ? 1 : 0));
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[i].c_str(), EpdFontFamily::SUP);
const int screenWidth = renderer.getScreenWidth();
int rubyX = 0;
int groupDrawX = 0;
if (rubyWidth > groupActualWidth) {
rubyX = leaderWordX_shifted - (rubyWidth - groupActualWidth) / 2;
if (i == 0) {
rubyX = std::max(leaderWordX_shifted, rubyX);
}
if (rubyX < lastEnd) {
rubyX = lastEnd;
}
groupDrawX = rubyX + (rubyWidth - groupActualWidth) / 2;
} else {
groupDrawX = leaderWordX_shifted;
rubyX = groupDrawX + (groupActualWidth - rubyWidth) / 2;
if (i == 0) {
rubyX = std::max(leaderWordX_shifted, rubyX);
}
if (rubyX < lastEnd) {
const int push = lastEnd - rubyX;
rubyX = lastEnd;
groupDrawX += push;
}
}
rubyX = std::max(0, std::min(rubyX, screenWidth - rubyWidth));
// Keep groupDrawX aligned if rubyX was clamped by screen edges
if (rubyWidth > groupActualWidth) {
groupDrawX = rubyX + (rubyWidth - groupActualWidth) / 2;
}
rubies[i] = {rubyX, rubyWidth, rubyTexts[i], baseDir};
lastEnd = rubyX + rubyWidth;
// Propagate shift to all words in the group and subsequent words
const int groupShift = groupDrawX - leaderWordX;
accumulatedShift = groupShift;
for (int k = 0; k < groupWordCount; ++k) {
wordShiftArr[i + k] = accumulatedShift;
}
const int leaderWordX = xposArr[i] + x;
const auto baseDir =
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(wordText(i), blockStyle.isRtl ? 1 : 0));
rubies[i] = {leaderWordX - (rubyWidth - groupActualWidth) / 2, rubyTexts[i], baseDir};
i += groupWordCount - 1;
}
}
@@ -275,7 +221,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
wordY += ascender / 4;
}
const int drawX = wordX + (blockHasRuby ? wordShiftArr[i] : 0);
const int drawX = wordX;
if (boundary > 0) {
// Focus split: draw bold prefix, then the regular suffix at a pre-computed x offset.
+14
View File
@@ -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
*/
+38 -23
View File
@@ -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() {
Loaded 20 of 103 files, more files were not shown because too many files have changed in this diff. Show more