Files
Crosspoint/lib/EpdFont/FontDecompressor.cpp
T

586 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "FontDecompressor.h"
#include <Arduino.h>
#include <Logging.h>
#include <Utf8.h>
#include <cstdlib>
#include <cstring>
FontDecompressor::~FontDecompressor() { deinit(); }
bool FontDecompressor::init() {
clearCache();
return true;
}
void FontDecompressor::deinit() { freePageBuffer(); }
void FontDecompressor::clearCache() { freePageBuffer(); }
void FontDecompressor::freePageBuffer() {
for (uint8_t s = 0; s < pageSlotCount; s++) {
free(pageSlots[s].buffer);
free(pageSlots[s].glyphs);
pageSlots[s] = {};
}
pageSlotCount = 0;
}
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
// O(1) path for frequency-grouped fonts with glyphToGroup mapping
if (fontData->glyphToGroup != nullptr) {
return fontData->glyphToGroup[glyphIndex];
}
// Contiguous-group fonts: linear scan
for (uint16_t i = 0; i < fontData->groupCount; i++) {
uint32_t first = fontData->groups[i].firstGlyphIndex;
if (glyphIndex >= first && glyphIndex < first + fontData->groups[i].glyphCount) {
return i;
}
}
return fontData->groupCount; // sentinel = not found
}
bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf,
uint32_t outSize) {
const EpdFontGroup& group = fontData->groups[groupIndex];
const uint32_t tDecomp = millis();
inflateReader.init(false);
inflateReader.setSource(&fontData->bitmap[group.compressedOffset], group.compressedSize);
if (!inflateReader.read(outBuf, outSize)) {
stats.decompressTimeMs += millis() - tDecomp;
LOG_ERR("FDC", "Decompression failed for group %u", groupIndex);
return false;
}
stats.decompressTimeMs += millis() - tDecomp;
return true;
}
// --- Byte-aligned helpers ---
uint32_t FontDecompressor::getAlignedOffset(const EpdFontData* fontData, uint16_t groupIndex, uint32_t glyphIndex) {
uint32_t offset = 0;
auto accumGlyph = [&](const EpdGlyph& g) {
if (g.width > 0 && g.height > 0) {
offset += ((g.width + 3) / 4) * g.height;
}
};
if (fontData->glyphToGroup) {
// Frequency-grouped: scan glyphs before glyphIndex that belong to this group
for (uint32_t i = 0; i < glyphIndex; i++) {
if (fontData->glyphToGroup[i] == groupIndex) {
accumGlyph(fontData->glyph[i]);
}
}
} else {
// Contiguous-group: sum aligned sizes of preceding glyphs in the group
const EpdFontGroup& group = fontData->groups[groupIndex];
for (uint32_t i = group.firstGlyphIndex; i < glyphIndex; i++) {
accumGlyph(fontData->glyph[i]);
}
}
return offset;
}
void FontDecompressor::compactSingleGlyph(const uint8_t* alignedSrc, uint8_t* packedDst, uint8_t width,
uint8_t height) {
if (width == 0 || height == 0) return;
const uint32_t rowStride = (width + 3) / 4;
if (width % 4 == 0) {
memcpy(packedDst, alignedSrc, rowStride * height);
return;
}
uint8_t outByte = 0, outBits = 0;
uint32_t writeIdx = 0;
for (uint8_t y = 0; y < height; y++) {
for (uint8_t x = 0; x < width; x++) {
outByte = (outByte << 2) | ((alignedSrc[y * rowStride + x / 4] >> ((3 - (x % 4)) * 2)) & 0x3);
outBits += 2;
if (outBits == 8) {
packedDst[writeIdx++] = outByte;
outByte = 0;
outBits = 0;
}
}
}
if (outBits > 0) packedDst[writeIdx] = outByte << (8 - outBits);
}
// --- getBitmap: page buffer → transient malloc + decompress + compact ---
const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex) {
const uint32_t tStart = micros();
stats.getBitmapCalls++;
if (!fontData->groups || fontData->groupCount == 0) {
stats.getBitmapTimeUs += micros() - tStart;
return &fontData->bitmap[glyph->dataOffset];
}
// Check page buffer slots (populated by prewarmCache — one slot per font style)
for (uint8_t s = 0; s < pageSlotCount; s++) {
const auto& slot = pageSlots[s];
if (slot.fontData != fontData || slot.glyphCount == 0) continue;
int left = 0, right = slot.glyphCount - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (slot.glyphs[mid].glyphIndex == glyphIndex) {
if (slot.glyphs[mid].bufferOffset != UINT32_MAX) {
stats.cacheHits++;
stats.getBitmapTimeUs += micros() - tStart;
return &slot.buffer[slot.glyphs[mid].bufferOffset];
}
break; // Not extracted during prewarm; fall through to hot-group path
}
if (slot.glyphs[mid].glyphIndex < glyphIndex)
left = mid + 1;
else
right = mid - 1;
}
break; // Found the right slot but glyph wasn't in it; don't check other slots
}
// Check fallback LRU cache
for (uint8_t i = 0; i < FALLBACK_CACHE_SLOTS; i++) {
if (_fallbackCache[i].fontData == fontData && _fallbackCache[i].glyphIndex == glyphIndex) {
_fallbackCache[i].lastUsedTick = ++_fallbackTick;
stats.cacheHits++;
stats.fallbackCacheHits++;
stats.getBitmapTimeUs += micros() - tStart;
return _fallbackCache[i].buffer;
}
}
stats.fallbackCacheMisses++;
// Fallback: glyph wasn't in the page buffer — decompress its group transiently.
// This is the rare path (prewarm should cover all glyphs on a normal page).
uint16_t groupIndex = getGroupIndex(fontData, glyphIndex);
if (groupIndex >= fontData->groupCount) {
LOG_ERR("FDC", "Glyph %u not found in any group", glyphIndex);
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
stats.cacheMisses++;
const EpdFontGroup& group = fontData->groups[groupIndex];
if (glyph->dataLength > HOT_GLYPH_BUF_SIZE) {
LOG_ERR("FDC", "Glyph dataLength %u exceeds HOT_GLYPH_BUF_SIZE %u", glyph->dataLength, HOT_GLYPH_BUF_SIZE);
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
if (group.uncompressedSize > stats.peakTempBytes) stats.peakTempBytes = group.uncompressedSize;
uint8_t* groupBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
if (!groupBuf) {
LOG_ERR("FDC", "OOM: cannot allocate %lu bytes for group %u fallback", group.uncompressedSize, groupIndex);
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
if (!decompressGroup(fontData, groupIndex, groupBuf, group.uncompressedSize)) {
free(groupBuf);
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
uint8_t lruIndex = 0;
uint32_t oldestTick = UINT32_MAX;
for (uint8_t i = 0; i < FALLBACK_CACHE_SLOTS; i++) {
if (_fallbackCache[i].lastUsedTick < oldestTick) {
oldestTick = _fallbackCache[i].lastUsedTick;
lruIndex = i;
}
}
compactSingleGlyph(&groupBuf[alignedOff], _fallbackCache[lruIndex].buffer, glyph->width, glyph->height);
free(groupBuf);
_fallbackCache[lruIndex].fontData = fontData;
_fallbackCache[lruIndex].glyphIndex = glyphIndex;
_fallbackCache[lruIndex].lastUsedTick = ++_fallbackTick;
stats.getBitmapTimeUs += micros() - tStart;
return _fallbackCache[lruIndex].buffer;
}
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
int32_t FontDecompressor::findGlyphIndex(const EpdFontData* fontData, uint32_t codepoint) {
const EpdUnicodeInterval* intervals = fontData->intervals;
const int count = fontData->intervalCount;
if (count == 0) return -1;
// Binary search
int left = 0;
int right = count - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
const EpdUnicodeInterval* interval = &intervals[mid];
if (codepoint < interval->first) {
right = mid - 1;
} else if (codepoint > interval->last) {
left = mid + 1;
} else {
return static_cast<int32_t>(interval->offset + (codepoint - interval->first));
}
}
return -1;
}
int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8Text) {
if (!fontData || !fontData->groups || !utf8Text) return 0;
// Step 1: Collect unique glyph indices needed for this page
uint32_t neededGlyphs[MAX_PAGE_GLYPHS];
uint16_t glyphCount = 0;
bool glyphCapWarned = false;
const unsigned char* p = reinterpret_cast<const unsigned char*>(utf8Text);
while (*p) {
uint32_t cp = utf8NextCodepoint(&p);
if (cp == 0) break;
int32_t glyphIdx = findGlyphIndex(fontData, cp);
if (glyphIdx < 0) continue;
const EpdGlyph& glyph = fontData->glyph[glyphIdx];
// Whitespace/empty glyphs have no bitmap payload and do not need prewarm storage.
if (glyph.dataLength == 0 || glyph.width == 0 || glyph.height == 0) continue;
// Deduplicate against already prewarmed slots
bool alreadyCached = false;
for (uint8_t s = 0; s < pageSlotCount; s++) {
if (pageSlots[s].fontData != fontData || pageSlots[s].glyphCount == 0) continue;
int left = 0, right = pageSlots[s].glyphCount - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (pageSlots[s].glyphs[mid].glyphIndex == static_cast<uint32_t>(glyphIdx)) {
if (pageSlots[s].glyphs[mid].bufferOffset != UINT32_MAX) {
alreadyCached = true;
}
break;
}
if (pageSlots[s].glyphs[mid].glyphIndex < static_cast<uint32_t>(glyphIdx))
left = mid + 1;
else
right = mid - 1;
}
if (alreadyCached) break;
}
if (alreadyCached) continue;
// Deduplicate within the current page pass
bool found = false;
for (uint16_t i = 0; i < glyphCount; i++) {
if (neededGlyphs[i] == static_cast<uint32_t>(glyphIdx)) {
found = true;
break;
}
}
if (!found) {
if (glyphCount < MAX_PAGE_GLYPHS) {
neededGlyphs[glyphCount++] = static_cast<uint32_t>(glyphIdx);
} else if (!glyphCapWarned) {
LOG_DBG("FDC", "Glyph cap (%u) reached during prewarm; excess glyphs will use hot-group fallback",
MAX_PAGE_GLYPHS);
glyphCapWarned = true;
}
}
}
// Add ligature output glyphs: if both input codepoints of a ligature pair are
// in the needed set, the output glyph will be queried during rendering.
// Must run BEFORE the neededGlyphGroups[] parallel-array loop below so appended
// glyphs receive a group index — otherwise hot-group lookup misses them.
if (fontData->ligaturePairs && fontData->ligaturePairCount > 0) {
for (uint32_t li = 0; li < fontData->ligaturePairCount && glyphCount < MAX_PAGE_GLYPHS; li++) {
uint32_t leftCp = fontData->ligaturePairs[li].pair >> 16;
uint32_t rightCp = fontData->ligaturePairs[li].pair & 0xFFFF;
int32_t leftIdx = findGlyphIndex(fontData, leftCp);
int32_t rightIdx = findGlyphIndex(fontData, rightCp);
if (leftIdx < 0 || rightIdx < 0) continue;
bool hasLeft = false, hasRight = false;
for (uint16_t i = 0; i < glyphCount; i++) {
if (neededGlyphs[i] == static_cast<uint32_t>(leftIdx)) hasLeft = true;
if (neededGlyphs[i] == static_cast<uint32_t>(rightIdx)) hasRight = true;
if (hasLeft && hasRight) break;
}
if (!hasLeft || !hasRight) continue;
int32_t outIdx = findGlyphIndex(fontData, fontData->ligaturePairs[li].ligatureCp);
if (outIdx < 0) continue;
bool found = false;
for (uint16_t i = 0; i < glyphCount; i++) {
if (neededGlyphs[i] == static_cast<uint32_t>(outIdx)) {
found = true;
break;
}
}
if (!found) {
neededGlyphs[glyphCount++] = static_cast<uint32_t>(outIdx);
}
}
}
if (glyphCount == 0) return 0;
// Allocate the next available slot
if (pageSlotCount >= MAX_PAGE_SLOTS) {
LOG_ERR("FDC", "All %u page buffer slots full, cannot prewarm fontData=%p", MAX_PAGE_SLOTS, (void*)fontData);
return -1;
}
PageSlot& slot = pageSlots[pageSlotCount];
// Step 2: Compute total buffer size and collect unique groups
uint32_t totalBytes = 0;
uint16_t neededGroups[128];
uint16_t neededGlyphGroups[MAX_PAGE_GLYPHS]; // parallel to neededGlyphs; avoids re-calling getGroupIndex later
uint8_t groupCount = 0;
bool groupCapWarned = false;
for (uint16_t i = 0; i < glyphCount; i++) {
totalBytes += fontData->glyph[neededGlyphs[i]].dataLength;
const uint16_t gi = getGroupIndex(fontData, neededGlyphs[i]);
neededGlyphGroups[i] = gi;
bool found = false;
for (uint8_t j = 0; j < groupCount; j++) {
if (neededGroups[j] == gi) {
found = true;
break;
}
}
if (!found) {
if (groupCount < 128) {
neededGroups[groupCount++] = gi;
} else if (!groupCapWarned) {
LOG_DBG("FDC", "Group cap (128) reached during prewarm; some groups will use hot-group fallback");
groupCapWarned = true;
}
}
}
stats.uniqueGroupsAccessed = groupCount;
// Safety: if the collected glyph set has no bitmap payload, skip slot allocation.
if (totalBytes == 0) {
LOG_DBG("FDC", "Prewarm skipped: %u glyphs but 0 bitmap bytes", glyphCount);
return 0;
}
// Sort neededGroups by ascending group index so flash reads are sequential.
// Uses insertion sort — groupCount is bounded at 128, typically <14 for Latin fonts.
for (uint8_t i = 1; i < groupCount; i++) {
uint16_t key = neededGroups[i];
int j = i - 1;
while (j >= 0 && neededGroups[j] > key) {
neededGroups[j + 1] = neededGroups[j];
j--;
}
neededGroups[j + 1] = key;
}
// Step 3: Allocate page buffer and lookup table for this slot
slot.buffer = static_cast<uint8_t*>(malloc(totalBytes));
slot.glyphs = static_cast<PageGlyphEntry*>(malloc(glyphCount * sizeof(PageGlyphEntry)));
if (!slot.buffer || !slot.glyphs) {
LOG_ERR("FDC", "Failed to allocate page buffer (%u bytes, %u glyphs)", totalBytes, glyphCount);
free(slot.buffer);
free(slot.glyphs);
slot = {};
return glyphCount;
}
stats.pageBufferBytes += totalBytes;
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
slot.fontData = fontData;
slot.glyphCount = glyphCount;
pageSlotCount++;
// Initialize lookup entries (bufferOffset = UINT32_MAX means not yet extracted)
for (uint16_t i = 0; i < glyphCount; i++) {
slot.glyphs[i] = {neededGlyphs[i], UINT32_MAX, 0, neededGlyphGroups[i]};
}
// Sort by glyphIndex for binary search in getBitmap()
for (uint16_t i = 1; i < glyphCount; i++) {
PageGlyphEntry key = slot.glyphs[i];
int j = i - 1;
while (j >= 0 && slot.glyphs[j].glyphIndex > key.glyphIndex) {
slot.glyphs[j + 1] = slot.glyphs[j];
j--;
}
slot.glyphs[j + 1] = key;
}
// Step 3b: Pre-scan to compute each needed glyph's byte-aligned offset within its group.
// This avoids recomputing aligned offsets per group during extraction in step 4.
uint32_t groupAlignedTracker[128] = {}; // running byte-aligned offset for each needed group
if (fontData->glyphToGroup) {
// Frequency-grouped: single O(totalGlyphs) pass through glyphToGroup.
// Reverse map (fontGroupIdx → position in neededGroups) replaces the inner
// linear scan, dropping this pass from O(totalGlyphs × groupCount) to O(totalGlyphs).
uint8_t* groupIdToPos = static_cast<uint8_t*>(malloc(fontData->groupCount));
if (!groupIdToPos) {
LOG_ERR("FDC", "OOM: cannot allocate %u bytes for groupIdToPos map", fontData->groupCount);
// Roll back this slot only (other slots from prior prewarmCache calls stay valid)
stats.pageBufferBytes -= totalBytes;
stats.pageGlyphsBytes -= glyphCount * sizeof(PageGlyphEntry);
free(slot.buffer);
free(slot.glyphs);
slot = {};
pageSlotCount--;
return glyphCount;
}
memset(groupIdToPos, 0xFF, fontData->groupCount);
for (uint8_t j = 0; j < groupCount; j++) groupIdToPos[neededGroups[j]] = j;
const auto& lastInterval = fontData->intervals[fontData->intervalCount - 1];
const uint32_t totalGlyphs = lastInterval.offset + (lastInterval.last - lastInterval.first + 1);
for (uint32_t i = 0; i < totalGlyphs; i++) {
const uint16_t gi = fontData->glyphToGroup[i];
const uint8_t gpPos = groupIdToPos[gi];
if (gpPos == 0xFF) continue; // not a needed group
const EpdGlyph& glyph = fontData->glyph[i];
// Binary search in sorted slot.glyphs to find if glyph i is needed
int left = 0, right = (int)slot.glyphCount - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
if (slot.glyphs[mid].glyphIndex == i) {
slot.glyphs[mid].alignedOffset = groupAlignedTracker[gpPos];
break;
}
if (slot.glyphs[mid].glyphIndex < i)
left = mid + 1;
else
right = mid - 1;
}
if (glyph.width > 0 && glyph.height > 0) {
groupAlignedTracker[gpPos] += ((glyph.width + 3) / 4) * glyph.height;
}
}
free(groupIdToPos);
} else {
// Contiguous-group: iterate each needed group's glyphs directly
for (uint8_t g = 0; g < groupCount; g++) {
const EpdFontGroup& group = fontData->groups[neededGroups[g]];
uint32_t alignedOff = 0;
for (uint16_t j = 0; j < group.glyphCount; j++) {
const uint32_t glyphI = group.firstGlyphIndex + j;
const EpdGlyph& glyph = fontData->glyph[glyphI];
int left = 0, right = (int)slot.glyphCount - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
if (slot.glyphs[mid].glyphIndex == glyphI) {
slot.glyphs[mid].alignedOffset = alignedOff;
break;
}
if (slot.glyphs[mid].glyphIndex < glyphI)
left = mid + 1;
else
right = mid - 1;
}
if (glyph.width > 0 && glyph.height > 0) {
alignedOff += ((glyph.width + 3) / 4) * glyph.height;
}
}
}
}
// Step 4: For each unique group, malloc a transient buffer, decompress, extract needed glyphs, free.
// One malloc/free per group per prewarm call. Groups are visited in sorted order, so
// only one group buffer is alive at a time — peak heap = page buffer + largest single group.
uint32_t writeOffset = 0;
int missed = 0;
for (uint8_t g = 0; g < groupCount; g++) {
uint16_t groupIdx = neededGroups[g];
const EpdFontGroup& group = fontData->groups[groupIdx];
if (group.uncompressedSize > stats.peakTempBytes) stats.peakTempBytes = group.uncompressedSize;
uint8_t* groupBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
if (!groupBuf) {
LOG_ERR("FDC", "OOM: cannot allocate %lu bytes for group %u during prewarm", group.uncompressedSize, groupIdx);
missed++;
continue;
}
if (!decompressGroup(fontData, groupIdx, groupBuf, group.uncompressedSize)) {
free(groupBuf);
missed++;
continue;
}
// Extract needed glyphs directly from the byte-aligned buffer, compacting on the fly.
// alignedOffset was pre-computed in step 3b — no full-group compact scan needed.
for (uint16_t i = 0; i < slot.glyphCount; i++) {
if (slot.glyphs[i].bufferOffset != UINT32_MAX) continue; // already extracted
if (slot.glyphs[i].groupIndex != groupIdx) continue;
const EpdGlyph& glyph = fontData->glyph[slot.glyphs[i].glyphIndex];
compactSingleGlyph(&groupBuf[slot.glyphs[i].alignedOffset], &slot.buffer[writeOffset], glyph.width, glyph.height);
slot.glyphs[i].bufferOffset = writeOffset;
writeOffset += glyph.dataLength;
}
free(groupBuf);
}
LOG_DBG("FDC", "Prewarm: %u glyphs in %u bytes from %u groups (%d missed)", glyphCount, writeOffset, groupCount,
missed);
return missed;
}
// --- Stats ---
void FontDecompressor::resetStats() { stats = Stats{}; }
void FontDecompressor::logStats(const char* label) {
const uint32_t total = stats.cacheHits + stats.cacheMisses;
LOG_DBG("FDC", "[%s] hits=%lu misses=%lu (%.1f%% hit rate)", label, stats.cacheHits, stats.cacheMisses,
total > 0 ? 100.0f * stats.cacheHits / total : 0.0f);
LOG_DBG("FDC", "[%s] decompress=%lums groups_accessed=%u", label, stats.decompressTimeMs, stats.uniqueGroupsAccessed);
LOG_DBG("FDC", "[%s] mem: pageBuf=%lu pageGlyphs=%lu peakTemp=%lu", label, stats.pageBufferBytes,
stats.pageGlyphsBytes, stats.peakTempBytes);
if (stats.getBitmapCalls > 0) {
LOG_DBG("FDC", "[%s] getBitmap: %lu calls, %luus total, %luus/call avg", label, stats.getBitmapCalls,
stats.getBitmapTimeUs, stats.getBitmapTimeUs / stats.getBitmapCalls);
}
uint32_t lruTotal = stats.fallbackCacheHits + stats.fallbackCacheMisses;
if (lruTotal > 0) {
LOG_DBG("FDC", "[%s] LRU Fallback: hits=%lu misses=%lu (%.1f%%)", label, stats.fallbackCacheHits,
stats.fallbackCacheMisses, 100.0f * stats.fallbackCacheHits / lruTotal);
}
resetStats();
}