Files
crosspoint-reader-mod/lib/EpdFont/FontDecompressor.cpp

148 lines
4.1 KiB
C++
Raw Permalink Normal View History

perf: Reduce overall flash usage by 30.7% by compressing built-in fonts (#831) **What is the goal of this PR?** Compress reader font bitmaps to reduce flash usage by 30.7%. **What changes are included?** - New `EpdFontGroup` struct and extended `EpdFontData` with `groups`/`groupCount` fields - `--compress` flag in `fontconvert.py`: groups glyphs (ASCII base group + groups of 8) and compresses each with raw DEFLATE - `FontDecompressor` class with 4-slot LRU cache for on-demand decompression during rendering - `GfxRenderer` transparently routes bitmap access through `getGlyphBitmap()` (compressed or direct flash) - Uses `uzlib` for decompression with minimal heap overhead. - 48 reader fonts (Bookerly, NotoSans 12-18pt, OpenDyslexic) regenerated with compression; 5 UI fonts unchanged - Round-trip verification script (`verify_compression.py`) runs as part of font generation | | baseline | font-compression | Difference | |--|--------|-----------------|------------| | Flash (ELF) | 6,302,476 B (96.2%) | 4,365,022 B (66.6%) | -1,937,454 B (-30.7%) | | firmware.bin | 6,468,192 B | 4,531,008 B | -1,937,184 B (-29.9%) | | RAM | 101,700 B (31.0%) | 103,076 B (31.5%) | +1,376 B (+0.5%) | Comparison of uncompressed baseline vs script-based group compression (4-slot LRU cache, cleared each page). Glyphs are grouped by Unicode block (ASCII, Latin-1, Latin Extended-A, Combining Marks, Cyrillic, General Punctuation, etc.) instead of sequential groups of 8. | | Baseline | Compressed (cold cache) | Difference | |---|---|---|---| | **Median** | 414.9 ms | 431.6 ms | +16.7 ms (+4.0%) | | **Pages** | 37 | 37 | | | | Baseline | Compressed (cold cache) | Difference | |---|---|---|---| | **Heap free (median)** | 187.0 KB | 176.3 KB | -10.7 KB | | **Heap free (min)** | 186.0 KB | 166.5 KB | -19.5 KB | | **Largest block (median)** | 148.0 KB | 128.0 KB | -20.0 KB | | **Largest block (min)** | 148.0 KB | 120.0 KB | -28.0 KB | | | Misses/page | Hit rate | |---|---|---| | **Compressed (cold cache)** | 2.1 | 99.85% | ------ While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_ Implementation was done by Claude Code (Opus 4.6) based on a plan developed collaboratively. All generated font headers were verified with an automated round-trip decompression test. The firmware was compiled successfully but has not yet been tested on-device. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 20:30:15 +11:00
#include "FontDecompressor.h"
#include <Logging.h>
#include <uzlib.h>
#include <cstdlib>
#include <cstring>
bool FontDecompressor::init() {
clearCache();
memset(&decomp, 0, sizeof(decomp));
return true;
}
void FontDecompressor::freeAllEntries() {
for (auto& entry : cache) {
if (entry.data) {
free(entry.data);
entry.data = nullptr;
}
entry.valid = false;
}
}
void FontDecompressor::deinit() { freeAllEntries(); }
void FontDecompressor::clearCache() {
freeAllEntries();
accessCounter = 0;
}
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint16_t glyphIndex) {
for (uint16_t i = 0; i < fontData->groupCount; i++) {
uint16_t first = fontData->groups[i].firstGlyphIndex;
if (glyphIndex >= first && glyphIndex < first + fontData->groups[i].glyphCount) {
return i;
}
}
return fontData->groupCount; // sentinel = not found
}
FontDecompressor::CacheEntry* FontDecompressor::findInCache(const EpdFontData* fontData, uint16_t groupIndex) {
for (auto& entry : cache) {
if (entry.valid && entry.font == fontData && entry.groupIndex == groupIndex) {
return &entry;
}
}
return nullptr;
}
FontDecompressor::CacheEntry* FontDecompressor::findEvictionCandidate() {
// Find an invalid slot first
for (auto& entry : cache) {
if (!entry.valid) {
return &entry;
}
}
// Otherwise evict LRU
CacheEntry* lru = &cache[0];
for (auto& entry : cache) {
if (entry.lastUsed < lru->lastUsed) {
lru = &entry;
}
}
return lru;
}
bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, CacheEntry* entry) {
const EpdFontGroup& group = fontData->groups[groupIndex];
// Free old buffer if reusing a slot
if (entry->data) {
free(entry->data);
entry->data = nullptr;
}
entry->valid = false;
// Allocate output buffer
auto* outBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
if (!outBuf) {
LOG_ERR("FDC", "Failed to allocate %u bytes for group %u", group.uncompressedSize, groupIndex);
return false;
}
// Decompress using uzlib
const uint8_t* inputBuf = &fontData->bitmap[group.compressedOffset];
uzlib_uncompress_init(&decomp, NULL, 0);
decomp.source = inputBuf;
decomp.source_limit = inputBuf + group.compressedSize;
decomp.dest_start = outBuf;
decomp.dest = outBuf;
decomp.dest_limit = outBuf + group.uncompressedSize;
int res = uzlib_uncompress(&decomp);
if (res < 0 || decomp.dest != decomp.dest_limit) {
LOG_ERR("FDC", "Decompression failed for group %u (status %d)", groupIndex, res);
free(outBuf);
return false;
}
entry->font = fontData;
entry->groupIndex = groupIndex;
entry->data = outBuf;
entry->dataSize = group.uncompressedSize;
entry->valid = true;
return true;
}
const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint16_t glyphIndex) {
if (!fontData->groups || fontData->groupCount == 0) {
return &fontData->bitmap[glyph->dataOffset];
}
uint16_t groupIndex = getGroupIndex(fontData, glyphIndex);
if (groupIndex >= fontData->groupCount) {
LOG_ERR("FDC", "Glyph %u not found in any group", glyphIndex);
return nullptr;
}
// Check cache
CacheEntry* entry = findInCache(fontData, groupIndex);
if (entry) {
entry->lastUsed = ++accessCounter;
if (glyph->dataOffset + glyph->dataLength > entry->dataSize) {
LOG_ERR("FDC", "dataOffset %u + dataLength %u out of bounds for group %u (size %u)", glyph->dataOffset,
glyph->dataLength, groupIndex, entry->dataSize);
return nullptr;
}
return &entry->data[glyph->dataOffset];
}
// Cache miss - decompress
entry = findEvictionCandidate();
if (!decompressGroup(fontData, groupIndex, entry)) {
return nullptr;
}
entry->lastUsed = ++accessCounter;
if (glyph->dataOffset + glyph->dataLength > entry->dataSize) {
LOG_ERR("FDC", "dataOffset %u + dataLength %u out of bounds for group %u (size %u)", glyph->dataOffset,
glyph->dataLength, groupIndex, entry->dataSize);
return nullptr;
}
return &entry->data[glyph->dataOffset];
}