Merge pull request #120 from jpirnay/feat-rendering-perf
refactor: Font rendering performance improvements
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
FontDecompressor::~FontDecompressor() { deinit(); }
|
||||
|
||||
@@ -13,15 +14,9 @@ bool FontDecompressor::init() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void FontDecompressor::deinit() {
|
||||
freePageBuffer();
|
||||
freeHotGroup();
|
||||
}
|
||||
void FontDecompressor::deinit() { freePageBuffer(); }
|
||||
|
||||
void FontDecompressor::clearCache() {
|
||||
freePageBuffer();
|
||||
freeHotGroup();
|
||||
}
|
||||
void FontDecompressor::clearCache() { freePageBuffer(); }
|
||||
|
||||
void FontDecompressor::freePageBuffer() {
|
||||
for (uint8_t s = 0; s < pageSlotCount; s++) {
|
||||
@@ -32,15 +27,6 @@ void FontDecompressor::freePageBuffer() {
|
||||
pageSlotCount = 0;
|
||||
}
|
||||
|
||||
void FontDecompressor::freeHotGroup() {
|
||||
hotGroup.clear();
|
||||
hotGroup.shrink_to_fit();
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
hotGlyphBuf.clear();
|
||||
hotGlyphBuf.shrink_to_fit();
|
||||
}
|
||||
|
||||
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
|
||||
// O(1) path for frequency-grouped fonts with glyphToGroup mapping
|
||||
if (fontData->glyphToGroup != nullptr) {
|
||||
@@ -60,10 +46,10 @@ uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t g
|
||||
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);
|
||||
@@ -126,7 +112,7 @@ void FontDecompressor::compactSingleGlyph(const uint8_t* alignedSrc, uint8_t* pa
|
||||
if (outBits > 0) packedDst[writeIdx] = outByte << (8 - outBits);
|
||||
}
|
||||
|
||||
// --- getBitmap: page buffer → hot group → decompress ---
|
||||
// --- 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();
|
||||
@@ -161,7 +147,8 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
|
||||
break; // Found the right slot but glyph wasn't in it; don't check other slots
|
||||
}
|
||||
|
||||
// Fallback: hot group slot
|
||||
// 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);
|
||||
@@ -169,49 +156,36 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if hot group already has this group decompressed — if not, decompress it
|
||||
if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
|
||||
stats.cacheMisses++;
|
||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||
stats.cacheMisses++;
|
||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||
|
||||
hotGroup.resize(group.uncompressedSize);
|
||||
if (hotGroup.empty()) {
|
||||
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) {
|
||||
hotGroup.clear();
|
||||
hotGroup.shrink_to_fit();
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
hotGroupFont = fontData;
|
||||
hotGroupIndex = groupIndex;
|
||||
stats.hotGroupBytes = group.uncompressedSize;
|
||||
} else {
|
||||
stats.cacheHits++;
|
||||
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;
|
||||
}
|
||||
|
||||
// Compact just the requested glyph from byte-aligned data into scratch buffer
|
||||
if (glyph->dataLength > hotGlyphBuf.size()) {
|
||||
hotGlyphBuf.resize(glyph->dataLength);
|
||||
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 (hotGlyphBuf.empty()) {
|
||||
|
||||
if (!decompressGroup(fontData, groupIndex, groupBuf, group.uncompressedSize)) {
|
||||
free(groupBuf);
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
|
||||
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height);
|
||||
compactSingleGlyph(&groupBuf[alignedOff], _hotGlyphBuf, glyph->width, glyph->height);
|
||||
free(groupBuf);
|
||||
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return hotGlyphBuf.data();
|
||||
return _hotGlyphBuf;
|
||||
}
|
||||
|
||||
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
|
||||
@@ -289,12 +263,14 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
||||
// 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;
|
||||
uint16_t gi = getGroupIndex(fontData, neededGlyphs[i]);
|
||||
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) {
|
||||
@@ -314,6 +290,18 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
||||
|
||||
stats.uniqueGroupsAccessed = groupCount;
|
||||
|
||||
// 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)));
|
||||
@@ -333,7 +321,7 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
||||
|
||||
// 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};
|
||||
slot.glyphs[i] = {neededGlyphs[i], UINT32_MAX, 0, neededGlyphGroups[i]};
|
||||
}
|
||||
|
||||
// Sort by glyphIndex for binary search in getBitmap()
|
||||
@@ -352,21 +340,31 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
||||
uint32_t groupAlignedTracker[128] = {}; // running byte-aligned offset for each needed group
|
||||
|
||||
if (fontData->glyphToGroup) {
|
||||
// Frequency-grouped: single O(totalGlyphs) pass through 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];
|
||||
// Find this glyph's group position in neededGroups
|
||||
uint8_t gpPos = groupCount;
|
||||
for (uint8_t j = 0; j < groupCount; j++) {
|
||||
if (neededGroups[j] == gi) {
|
||||
gpPos = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (gpPos == groupCount) continue; // not a needed group
|
||||
const uint8_t gpPos = groupIdToPos[gi];
|
||||
if (gpPos == 0xFF) continue; // not a needed group
|
||||
|
||||
const EpdGlyph& glyph = fontData->glyph[i];
|
||||
|
||||
@@ -388,6 +386,8 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
||||
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++) {
|
||||
@@ -417,7 +417,9 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: For each unique group, decompress to temp buffer and extract needed glyphs
|
||||
// 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;
|
||||
|
||||
@@ -425,35 +427,34 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
||||
uint16_t groupIdx = neededGroups[g];
|
||||
const EpdFontGroup& group = fontData->groups[groupIdx];
|
||||
|
||||
auto* tempBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
|
||||
if (!tempBuf) {
|
||||
LOG_ERR("FDC", "Failed to allocate temp buffer (%u bytes) for group %u", group.uncompressedSize, groupIdx);
|
||||
missed++;
|
||||
continue;
|
||||
}
|
||||
if (group.uncompressedSize > stats.peakTempBytes) {
|
||||
stats.peakTempBytes = group.uncompressedSize;
|
||||
}
|
||||
if (group.uncompressedSize > stats.peakTempBytes) stats.peakTempBytes = group.uncompressedSize;
|
||||
|
||||
if (!decompressGroup(fontData, groupIdx, tempBuf, group.uncompressedSize)) {
|
||||
free(tempBuf);
|
||||
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;
|
||||
}
|
||||
|
||||
// Extract needed glyphs directly from the byte-aligned temp buffer, compacting on the fly.
|
||||
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 (getGroupIndex(fontData, slot.glyphs[i].glyphIndex) != groupIdx) continue;
|
||||
if (slot.glyphs[i].groupIndex != groupIdx) continue;
|
||||
|
||||
const EpdGlyph& glyph = fontData->glyph[slot.glyphs[i].glyphIndex];
|
||||
compactSingleGlyph(&tempBuf[slot.glyphs[i].alignedOffset], &slot.buffer[writeOffset], glyph.width, glyph.height);
|
||||
compactSingleGlyph(&groupBuf[slot.glyphs[i].alignedOffset], &slot.buffer[writeOffset], glyph.width, glyph.height);
|
||||
slot.glyphs[i].bufferOffset = writeOffset;
|
||||
writeOffset += glyph.dataLength;
|
||||
}
|
||||
|
||||
free(tempBuf);
|
||||
free(groupBuf);
|
||||
}
|
||||
|
||||
LOG_DBG("FDC", "Prewarm: %u glyphs in %u bytes from %u groups (%d missed)", glyphCount, writeOffset, groupCount,
|
||||
@@ -471,8 +472,8 @@ void FontDecompressor::logStats(const char* label) {
|
||||
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 hotGroup=%lu peakTemp=%lu", label, stats.pageBufferBytes,
|
||||
stats.pageGlyphsBytes, stats.hotGroupBytes, stats.peakTempBytes);
|
||||
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);
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
#include <InflateReader.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "EpdFontData.h"
|
||||
|
||||
class FontDecompressor {
|
||||
@@ -18,10 +16,14 @@ class FontDecompressor {
|
||||
void deinit();
|
||||
|
||||
// Returns pointer to decompressed bitmap data for the given glyph.
|
||||
// Checks the page buffer (from prewarm) first, then falls back to the hot group slot.
|
||||
// Checks the page buffer (from prewarm) first and otherwise transiently
|
||||
// allocates/decompresses the glyph's group into a temporary buffer and
|
||||
// compacts the requested glyph. The returned pointer is valid only until the
|
||||
// next getBitmap call or cache eviction; callers must copy bitmap data if a
|
||||
// longer lifetime is required.
|
||||
const uint8_t* getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex);
|
||||
|
||||
// Free all cached data (page buffer + hot group).
|
||||
// Free all cached data (page buffers).
|
||||
void clearCache();
|
||||
|
||||
// Pre-scan UTF-8 text and extract needed glyph bitmaps into a flat page buffer.
|
||||
@@ -36,8 +38,7 @@ class FontDecompressor {
|
||||
uint16_t uniqueGroupsAccessed = 0;
|
||||
uint32_t pageBufferBytes = 0; // pageBuffer allocation
|
||||
uint32_t pageGlyphsBytes = 0; // pageGlyphs lookup table allocation
|
||||
uint32_t hotGroupBytes = 0; // current hot group allocation
|
||||
uint32_t peakTempBytes = 0; // largest temp buffer in prewarm
|
||||
uint32_t peakTempBytes = 0; // largest temp buffer in prewarm or getBitmap miss
|
||||
uint32_t getBitmapTimeUs = 0; // cumulative getBitmap time (micros)
|
||||
uint32_t getBitmapCalls = 0; // number of getBitmap calls
|
||||
};
|
||||
@@ -55,6 +56,7 @@ class FontDecompressor {
|
||||
uint32_t glyphIndex;
|
||||
uint32_t bufferOffset;
|
||||
uint32_t alignedOffset; // byte-aligned offset within its decompressed group (set during prewarm pre-scan)
|
||||
uint16_t groupIndex; // cached to avoid re-calling getGroupIndex in prewarm Step 4
|
||||
};
|
||||
struct PageSlot {
|
||||
uint8_t* buffer = nullptr;
|
||||
@@ -65,18 +67,13 @@ class FontDecompressor {
|
||||
PageSlot pageSlots[MAX_PAGE_SLOTS] = {};
|
||||
uint8_t pageSlotCount = 0;
|
||||
|
||||
// Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path.
|
||||
// Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf.
|
||||
const EpdFontData* hotGroupFont = nullptr;
|
||||
uint16_t hotGroupIndex = UINT16_MAX;
|
||||
std::vector<uint8_t> hotGroup;
|
||||
static constexpr uint16_t HOT_GLYPH_BUF_SIZE = 512; // largest packed single glyph
|
||||
|
||||
// Scratch buffer for compacting a single glyph from the hot group.
|
||||
// Scratch buffer for compacting a single glyph after a getBitmap() miss.
|
||||
// Valid until the next getBitmap() call.
|
||||
std::vector<uint8_t> hotGlyphBuf;
|
||||
uint8_t _hotGlyphBuf[HOT_GLYPH_BUF_SIZE] = {};
|
||||
|
||||
void freePageBuffer();
|
||||
void freeHotGroup();
|
||||
uint16_t getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex);
|
||||
uint32_t getAlignedOffset(const EpdFontData* fontData, uint16_t groupIndex, uint32_t glyphIndex);
|
||||
bool decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf, uint32_t outSize);
|
||||
|
||||
@@ -726,6 +726,12 @@ if compress:
|
||||
# are grouped together for efficient LRU caching on the embedded target.
|
||||
# Since glyphs are in codepoint order, glyphs in the same Unicode block
|
||||
# are contiguous in the array and form natural groups.
|
||||
#
|
||||
# A hard size cap (GROUP_MAX_UNCOMPRESSED_BYTES) is applied on top of script
|
||||
# boundaries: if adding the next glyph would push the uncompressed group size
|
||||
# over the cap, the group is closed and a new one started with the same script
|
||||
# ID. This keeps the embedded decompressor's transient malloc bounded regardless
|
||||
# of font density (CJK, Vietnamese, user-supplied fonts with large Unicode blocks).
|
||||
SCRIPT_GROUP_RANGES = [
|
||||
(0x0000, 0x007F), # ASCII
|
||||
(0x0080, 0x00FF), # Latin-1 Supplement
|
||||
@@ -743,6 +749,10 @@ if compress:
|
||||
(0xFFFD, 0xFFFD), # Replacement Character
|
||||
]
|
||||
|
||||
# 64 KB cap: large enough to hold any single built-in font group with headroom,
|
||||
# small enough to be a comfortable transient malloc on the ESP32-C3.
|
||||
GROUP_MAX_UNCOMPRESSED_BYTES = 65536
|
||||
|
||||
def get_script_group(code_point):
|
||||
for i, (start, end) in enumerate(SCRIPT_GROUP_RANGES):
|
||||
if start <= code_point <= end:
|
||||
@@ -753,17 +763,28 @@ if compress:
|
||||
current_group_id = None
|
||||
group_start = 0
|
||||
group_count = 0
|
||||
group_uncompressed = 0
|
||||
|
||||
for i, (props, packed) in enumerate(all_glyphs):
|
||||
for i, (props, _) in enumerate(all_glyphs):
|
||||
sg = get_script_group(props.code_point)
|
||||
if sg != current_group_id:
|
||||
glyph_aligned_size = ((props.width + 3) // 4) * props.height if props.width > 0 and props.height > 0 else 0
|
||||
if glyph_aligned_size > GROUP_MAX_UNCOMPRESSED_BYTES:
|
||||
raise ValueError(
|
||||
f"Glyph {i} (code point U+{props.code_point:04X}) single aligned size "
|
||||
f"{glyph_aligned_size} exceeds GROUP_MAX_UNCOMPRESSED_BYTES={GROUP_MAX_UNCOMPRESSED_BYTES}"
|
||||
)
|
||||
size_overflow = group_uncompressed + glyph_aligned_size > GROUP_MAX_UNCOMPRESSED_BYTES
|
||||
|
||||
if sg != current_group_id or size_overflow:
|
||||
if group_count > 0:
|
||||
groups.append((group_start, group_count))
|
||||
current_group_id = sg
|
||||
group_start = i
|
||||
group_count = 1
|
||||
group_uncompressed = glyph_aligned_size
|
||||
else:
|
||||
group_count += 1
|
||||
group_uncompressed += glyph_aligned_size
|
||||
|
||||
if group_count > 0:
|
||||
groups.append((group_start, group_count))
|
||||
|
||||
@@ -17,6 +17,34 @@ constexpr int MAX_COST = std::numeric_limits<int>::max();
|
||||
|
||||
namespace {
|
||||
|
||||
// Closing punctuation that should not have extra space inserted before it during justification.
|
||||
// Includes common closing brackets/quotes and sentence-ending marks. En/em dashes
|
||||
// are also treated as inline separators here to avoid justification stretch
|
||||
// immediately before them.
|
||||
bool isClosingPunctuation(const uint32_t cp) {
|
||||
switch (cp) {
|
||||
case '.':
|
||||
case ',':
|
||||
case '!':
|
||||
case '?':
|
||||
case ':':
|
||||
case ';':
|
||||
case ')':
|
||||
case ']':
|
||||
case '}':
|
||||
case 0x00BB: // »
|
||||
case 0x203A: // ›
|
||||
case 0x2019: // ' right single quotation mark
|
||||
case 0x201D: // " right double quotation mark
|
||||
case 0x2026: // … ellipsis
|
||||
case 0x2013: // – en dash
|
||||
case 0x2014: // — em dash
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Soft hyphen byte pattern used throughout EPUBs (UTF-8 for U+00AD).
|
||||
constexpr char SOFT_HYPHEN_UTF8[] = "\xC2\xAD";
|
||||
constexpr size_t SOFT_HYPHEN_BYTES = 2;
|
||||
@@ -330,11 +358,19 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
|
||||
int cost;
|
||||
if (j == totalWordCount - 1) {
|
||||
cost = 0; // Last line
|
||||
cost = 0; // Last line — no penalty regardless of looseness
|
||||
} else {
|
||||
const int remainingSpace = effectivePageWidth - currlen;
|
||||
// Use long long for the square to prevent overflow
|
||||
const long long cost_ll = static_cast<long long>(remainingSpace) * remainingSpace + dp[j + 1];
|
||||
// Knuth-Plass style demerits:
|
||||
// badness = (gap/lineWidth)³ × 10000, clamped to [0, 10000]
|
||||
// demerits = (1 + badness)²
|
||||
// Cubic badness strongly penalises very loose lines while being
|
||||
// lenient on moderately loose ones, producing visually balanced paragraphs.
|
||||
const long long b_num = static_cast<long long>(remainingSpace) * remainingSpace * remainingSpace;
|
||||
const long long b_den = static_cast<long long>(effectivePageWidth) * effectivePageWidth * effectivePageWidth;
|
||||
const int badness = (b_den > 0) ? static_cast<int>(std::min(b_num * 10000LL / b_den, 10000LL)) : 10000;
|
||||
const long long demerits = static_cast<long long>(1 + badness) * (1 + badness);
|
||||
const long long cost_ll = demerits + dp[j + 1];
|
||||
|
||||
if (cost_ll > MAX_COST) {
|
||||
cost = MAX_COST;
|
||||
@@ -767,17 +803,19 @@ ParsedText::LineProcessResult ParsedText::extractLine(
|
||||
|
||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
|
||||
// Count gaps: each word after the first creates a gap, unless it's a continuation
|
||||
// Count gaps: each word after the first creates a gap, unless it's a continuation.
|
||||
// Gaps before closing punctuation (. , ) » etc.) are excluded from justification
|
||||
// distribution so they stay at natural space width.
|
||||
const uint32_t firstCp = firstCodepoint(words[lastBreakAt + wordIdx]);
|
||||
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
|
||||
actualGapCount++;
|
||||
totalNaturalGaps +=
|
||||
renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
|
||||
firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]);
|
||||
const bool beforeClosing = isClosingPunctuation(firstCp);
|
||||
if (!beforeClosing) actualGapCount++;
|
||||
totalNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]), firstCp,
|
||||
wordStyles[lastBreakAt + wordIdx - 1]);
|
||||
} else if (wordIdx > 0 && continuesVec[lastBreakAt + wordIdx]) {
|
||||
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
||||
totalNaturalGaps +=
|
||||
renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
|
||||
firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]);
|
||||
totalNaturalGaps += renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]), firstCp,
|
||||
wordStyles[lastBreakAt + wordIdx - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,12 +860,15 @@ ParsedText::LineProcessResult ParsedText::extractLine(
|
||||
} else {
|
||||
int gap = 0;
|
||||
if (wordIdx + 1 < lineWordCount) {
|
||||
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
|
||||
firstCodepoint(words[lastBreakAt + wordIdx + 1]),
|
||||
const uint32_t nextFirstCp = firstCodepoint(words[lastBreakAt + wordIdx + 1]);
|
||||
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx]), nextFirstCp,
|
||||
wordStyles[lastBreakAt + wordIdx]);
|
||||
}
|
||||
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine) {
|
||||
gap += justifyExtra;
|
||||
// Don't stretch the gap before closing punctuation — it looks wrong with
|
||||
// extra space before ".", ")", "»" etc.
|
||||
const bool nextIsClosing = isClosingPunctuation(nextFirstCp);
|
||||
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine && !nextIsClosing) {
|
||||
gap += justifyExtra;
|
||||
}
|
||||
}
|
||||
xpos += wordWidths[lastBreakAt + wordIdx] + gap;
|
||||
}
|
||||
|
||||
@@ -824,8 +824,7 @@ std::string EpubReaderActivity::buildRenderBenchmarkReport(const LastRenderStats
|
||||
std::to_string(endSnapshot.fontDecompressMs) + " ms, groups " +
|
||||
std::to_string(endSnapshot.fontUniqueGroups));
|
||||
appendLine("Font buffers: page " + std::to_string(endSnapshot.fontPageBufferBytes) + ", glyph table " +
|
||||
std::to_string(endSnapshot.fontPageGlyphsBytes) + ", hot group " +
|
||||
std::to_string(endSnapshot.fontHotGroupBytes) + ", peak temp " +
|
||||
std::to_string(endSnapshot.fontPageGlyphsBytes) + ", peak temp " +
|
||||
std::to_string(endSnapshot.fontPeakTempBytes));
|
||||
appendLine("Glyph lookups: " + std::to_string(endSnapshot.fontGetBitmapCalls) + " calls, " +
|
||||
std::to_string(endSnapshot.fontGetBitmapTimeUs) + " us total");
|
||||
@@ -1120,7 +1119,7 @@ int EpubReaderActivity::getEffectiveReaderFontId() const {
|
||||
}
|
||||
|
||||
bool EpubReaderActivity::stepPageState(const bool isForwardTurn) {
|
||||
if (!epub || !section || section->pageCount <= 0) {
|
||||
if (!epub || !section || section->pageCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1566,8 +1565,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
tEnd - t0);
|
||||
}
|
||||
|
||||
if (auto* cacheManager = renderer.getFontCacheManager()) {
|
||||
if (auto* decompressor = cacheManager->getDecompressor()) {
|
||||
if (const auto* cacheManager = renderer.getFontCacheManager()) {
|
||||
if (const auto* decompressor = cacheManager->getDecompressor()) {
|
||||
const auto& stats = decompressor->getStats();
|
||||
lastRenderStats.fontCacheHits = stats.cacheHits;
|
||||
lastRenderStats.fontCacheMisses = stats.cacheMisses;
|
||||
@@ -1575,7 +1574,6 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
lastRenderStats.fontUniqueGroups = stats.uniqueGroupsAccessed;
|
||||
lastRenderStats.fontPageBufferBytes = stats.pageBufferBytes;
|
||||
lastRenderStats.fontPageGlyphsBytes = stats.pageGlyphsBytes;
|
||||
lastRenderStats.fontHotGroupBytes = stats.hotGroupBytes;
|
||||
lastRenderStats.fontPeakTempBytes = stats.peakTempBytes;
|
||||
lastRenderStats.fontGetBitmapTimeUs = stats.getBitmapTimeUs;
|
||||
lastRenderStats.fontGetBitmapCalls = stats.getBitmapCalls;
|
||||
|
||||
@@ -85,7 +85,6 @@ class EpubReaderActivity final : public Activity {
|
||||
uint16_t fontUniqueGroups = 0;
|
||||
uint32_t fontPageBufferBytes = 0;
|
||||
uint32_t fontPageGlyphsBytes = 0;
|
||||
uint32_t fontHotGroupBytes = 0;
|
||||
uint32_t fontPeakTempBytes = 0;
|
||||
uint32_t fontGetBitmapTimeUs = 0;
|
||||
uint32_t fontGetBitmapCalls = 0;
|
||||
|
||||
Reference in New Issue
Block a user