perf: font-compression improvements (#1056)
## Purpose This PR includes some preparatory changes that are needed for an upcoming performant CJK font feature. The changes have no impact on render time and heap allocation for latin text. **Despite this, I think these changes stand on their own as a better font compression/decompression implementation.** ## Summary - Font decompressor rewrite: Replaced the 4-slot LRU group cache with a two-tier system — a page buffer (glyphs prewarmed before rendering begins) and a hot-group fallback (last decompressed group retained for non-prewarmed glyphs). - Byte-aligned compressed bitmap format: Glyph bitmaps within compressed groups are now stored row-padded rather than tightly packed before DEFLATE compression, improving compression ratios by making identical pixel rows produce identical byte patterns. Glyphs are compacted back to packed format on demand at render time. Reduces flash size by 155 KB. - Page prewarm system: Added `Page::collectText` and `Page::getDominantStyle` to extract per-style glyph requirements before rendering, and `GfxRenderer::prewarmFontCache` to pre-decompress only the groups needed for the dominant style — eliminating mid-render decompression for the common case. - UTF-8 robustness fixes: `utf8NextCodepoint` now validates continuation bytes and returns a replacement glyph on malformed input; `ChapterHtmlSlimParser` correctly preserves incomplete multi-byte sequences across word-buffer flush boundaries rather than splitting them. --- ### AI Usage 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**_ Architecture and design was done by me, refined a bit by Claude. Code mostly by Claude, but not entirely.
This commit is contained in:
committed by
GitHub
parent
b467ea7973
commit
f1e9dc7f30
96
lib/GfxRenderer/FontCacheManager.cpp
Normal file
96
lib/GfxRenderer/FontCacheManager.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
#include "FontCacheManager.h"
|
||||
|
||||
#include <FontDecompressor.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
FontCacheManager::FontCacheManager(const std::map<int, EpdFontFamily>& fontMap) : fontMap_(fontMap) {}
|
||||
|
||||
void FontCacheManager::setFontDecompressor(FontDecompressor* d) { fontDecompressor_ = d; }
|
||||
|
||||
void FontCacheManager::clearCache() {
|
||||
if (fontDecompressor_) fontDecompressor_->clearCache();
|
||||
}
|
||||
|
||||
void FontCacheManager::prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask) {
|
||||
if (!fontDecompressor_ || fontMap_.count(fontId) == 0) return;
|
||||
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
if (!(styleMask & (1 << i))) continue;
|
||||
auto style = static_cast<EpdFontFamily::Style>(i);
|
||||
const EpdFontData* data = fontMap_.at(fontId).getData(style);
|
||||
if (!data || !data->groups) continue;
|
||||
int missed = fontDecompressor_->prewarmCache(data, utf8Text);
|
||||
if (missed > 0) {
|
||||
LOG_DBG("FCM", "prewarmCache: %d glyph(s) not cached for style %d", missed, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FontCacheManager::logStats(const char* label) {
|
||||
if (fontDecompressor_) fontDecompressor_->logStats(label);
|
||||
}
|
||||
|
||||
void FontCacheManager::resetStats() {
|
||||
if (fontDecompressor_) fontDecompressor_->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;
|
||||
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++;
|
||||
}
|
||||
scanStyleCounts_[baseStyle] += cpCount;
|
||||
}
|
||||
|
||||
// --- PrewarmScope implementation ---
|
||||
|
||||
FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manager_(&manager) {
|
||||
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_));
|
||||
manager_->scanFontId_ = -1;
|
||||
}
|
||||
|
||||
void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
|
||||
manager_->scanMode_ = ScanMode::None;
|
||||
if (manager_->scanText_.empty()) return;
|
||||
|
||||
// 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);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
FontCacheManager::PrewarmScope::~PrewarmScope() {
|
||||
if (active_) {
|
||||
endScanAndPrewarm(); // no-op if already called (scanText_ is empty)
|
||||
manager_->clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
FontCacheManager::PrewarmScope::PrewarmScope(PrewarmScope&& other) noexcept
|
||||
: manager_(other.manager_), active_(other.active_) {
|
||||
other.active_ = false;
|
||||
}
|
||||
|
||||
FontCacheManager::PrewarmScope FontCacheManager::createPrewarmScope() { return PrewarmScope(*this); }
|
||||
55
lib/GfxRenderer/FontCacheManager.h
Normal file
55
lib/GfxRenderer/FontCacheManager.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
class FontDecompressor;
|
||||
|
||||
class FontCacheManager {
|
||||
public:
|
||||
explicit FontCacheManager(const std::map<int, EpdFontFamily>& fontMap);
|
||||
|
||||
void setFontDecompressor(FontDecompressor* d);
|
||||
|
||||
void clearCache();
|
||||
void prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F);
|
||||
void logStats(const char* label = "render");
|
||||
void resetStats();
|
||||
|
||||
// Scan-mode API: called by GfxRenderer::drawText() during scan pass
|
||||
bool isScanning() const;
|
||||
void recordText(const char* text, int fontId, EpdFontFamily::Style style);
|
||||
|
||||
// The FontDecompressor pointer, needed by GfxRenderer::getGlyphBitmap()
|
||||
FontDecompressor* getDecompressor() const { return fontDecompressor_; }
|
||||
|
||||
// RAII scope for two-pass prewarm pattern
|
||||
class PrewarmScope {
|
||||
public:
|
||||
explicit PrewarmScope(FontCacheManager& manager);
|
||||
~PrewarmScope();
|
||||
void endScanAndPrewarm();
|
||||
PrewarmScope(PrewarmScope&& other) noexcept;
|
||||
PrewarmScope& operator=(PrewarmScope&&) = delete;
|
||||
PrewarmScope(const PrewarmScope&) = delete;
|
||||
PrewarmScope& operator=(const PrewarmScope&) = delete;
|
||||
|
||||
private:
|
||||
FontCacheManager* manager_;
|
||||
bool active_ = true;
|
||||
};
|
||||
PrewarmScope createPrewarmScope();
|
||||
|
||||
private:
|
||||
const std::map<int, EpdFontFamily>& fontMap_;
|
||||
FontDecompressor* fontDecompressor_ = nullptr;
|
||||
|
||||
enum class ScanMode : uint8_t { None, Scanning };
|
||||
ScanMode scanMode_ = ScanMode::None;
|
||||
std::string scanText_;
|
||||
uint32_t scanStyleCounts_[4] = {};
|
||||
int scanFontId_ = -1;
|
||||
};
|
||||
@@ -1,16 +1,23 @@
|
||||
#include "GfxRenderer.h"
|
||||
|
||||
#include <FontDecompressor.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include "FontCacheManager.h"
|
||||
|
||||
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
|
||||
if (fontData->groups != nullptr) {
|
||||
if (!fontDecompressor) {
|
||||
auto* fd = fontCacheManager_ ? fontCacheManager_->getDecompressor() : nullptr;
|
||||
if (!fd) {
|
||||
LOG_ERR("GFX", "Compressed font but no FontDecompressor set");
|
||||
return nullptr;
|
||||
}
|
||||
uint16_t glyphIndex = static_cast<uint16_t>(glyph - fontData->glyph);
|
||||
return fontDecompressor->getBitmap(fontData, glyph, glyphIndex);
|
||||
uint32_t glyphIndex = static_cast<uint32_t>(glyph - fontData->glyph);
|
||||
// For page-buffer hits the pointer is stable for the page lifetime.
|
||||
// For hot-group hits it is valid only until the next getBitmap() call — callers
|
||||
// must consume it (draw the glyph) before requesting another bitmap.
|
||||
return fd->getBitmap(fontData, glyph, glyphIndex);
|
||||
}
|
||||
return &fontData->bitmap[glyph->dataOffset];
|
||||
}
|
||||
@@ -211,6 +218,11 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
return;
|
||||
}
|
||||
|
||||
if (fontCacheManager_ && fontCacheManager_->isScanning()) {
|
||||
fontCacheManager_->recordText(text, fontId, style);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
@@ -257,6 +269,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
}
|
||||
|
||||
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
|
||||
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
|
||||
if (x1 == x2) {
|
||||
if (y2 < y1) {
|
||||
std::swap(y1, y2);
|
||||
@@ -569,6 +582,7 @@ void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, con
|
||||
|
||||
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
|
||||
const float cropX, const float cropY) const {
|
||||
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
|
||||
// For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit)
|
||||
if (bitmap.is1Bit() && cropX == 0.0f && cropY == 0.0f) {
|
||||
drawBitmap1Bit(bitmap, x, y, maxWidth, maxHeight);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <EpdFontFamily.h>
|
||||
#include <FontDecompressor.h>
|
||||
#include <HalDisplay.h>
|
||||
|
||||
class FontCacheManager;
|
||||
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -39,7 +41,14 @@ class GfxRenderer {
|
||||
uint8_t* frameBuffer = nullptr;
|
||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||
std::map<int, EpdFontFamily> fontMap;
|
||||
FontDecompressor* fontDecompressor = nullptr;
|
||||
|
||||
// Mutable because drawText() is const but needs to delegate scan-mode
|
||||
// recording to the (non-const) FontCacheManager. Same pragmatic compromise
|
||||
// as before, concentrated in a single pointer instead of four fields.
|
||||
mutable FontCacheManager* fontCacheManager_ = nullptr;
|
||||
|
||||
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState,
|
||||
EpdFontFamily::Style style) const;
|
||||
void freeBwBufferChunks();
|
||||
template <Color color>
|
||||
void drawPixelDither(int x, int y) const;
|
||||
@@ -59,10 +68,9 @@ class GfxRenderer {
|
||||
// Setup
|
||||
void begin(); // must be called right after display.begin()
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
void setFontDecompressor(FontDecompressor* d) { fontDecompressor = d; }
|
||||
void clearFontCache() {
|
||||
if (fontDecompressor) fontDecompressor->clearCache();
|
||||
}
|
||||
void setFontCacheManager(FontCacheManager* m) { fontCacheManager_ = m; }
|
||||
FontCacheManager* getFontCacheManager() const { return fontCacheManager_; }
|
||||
const std::map<int, EpdFontFamily>& getFontMap() const { return fontMap; }
|
||||
|
||||
// Orientation control (affects logical width/height and coordinate transforms)
|
||||
void setOrientation(const Orientation o) { orientation = o; }
|
||||
|
||||
Reference in New Issue
Block a user