Merge pull request #120 from jpirnay/feat-rendering-perf

refactor: Font rendering performance improvements
This commit is contained in:
jpirnay
2026-04-24 19:50:11 +02:00
committed by GitHub
6 changed files with 179 additions and 122 deletions
+84 -83
View File
@@ -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);
+11 -14
View File
@@ -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);
+23 -2
View File
@@ -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))
+57 -16
View File
@@ -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;
}
+4 -6
View File
@@ -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;