Merge remote-tracking branch 'origin/develop' into feat-bluetooth

This commit is contained in:
Justin Mitchell
2026-07-14 04:20:50 -04:00
118 changed files with 22879 additions and 6746 deletions
+114 -26
View File
@@ -1,6 +1,7 @@
#include "GfxRenderer.h"
#include <BidiUtils.h>
#include <BuildScratch.h>
#include <FontDecompressor.h>
#include <HalGPIO.h>
#include <Logging.h>
@@ -24,6 +25,36 @@ uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style st
namespace {
const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir);
// Appends the shaped visual form of every RTL token in `text` to `shapedOut`.
// getTextAdvanceX() measures the bidi-reordered, Arabic-shaped codepoint stream,
// so the SD advance table must be warmed with the presentation forms as well as
// the logical codepoints — otherwise every RTL word measurement misses the fast
// path and falls through to onGlyphMiss(), which opens the .cpfont and reads
// glyph metadata + bitmap into the 8-slot overflow ring, once per glyph.
// Tokens without RTL lead bytes (0xD6-0xDB) are skipped with a byte scan, so
// pure-LTR text pays almost nothing.
void appendShapedRtlTokens(const char* text, std::string& shapedOut) {
const auto isBreak = [](const char c) { return c == ' ' || c == '\n' || c == '\r' || c == '\t'; };
std::string token;
std::string visual;
const char* p = text;
while (*p) {
while (*p && isBreak(*p)) ++p;
const char* start = p;
bool hasRtlBytes = false;
while (*p && !isBreak(*p)) {
const auto b = static_cast<unsigned char>(*p);
hasRtlBytes = hasRtlBytes || (b >= 0xD6 && b <= 0xDB);
++p;
}
if (!hasRtlBytes) continue;
token.assign(start, p - start);
if (BidiUtils::applyBidiVisual(token.c_str(), visual, static_cast<int>(BidiUtils::BidiBaseDir::AUTO))) {
shapedOut += visual;
}
}
}
} // namespace
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
@@ -57,7 +88,9 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
auto it = sdCardFonts_.find(fontId);
if (it != sdCardFonts_.end()) {
int missed = it->second->buildAdvanceTable(utf8Text, styleMask);
std::string shaped;
appendShapedRtlTokens(utf8Text, shaped);
int missed = it->second->buildAdvanceTable(utf8Text, styleMask, shaped.empty() ? nullptr : shaped.c_str());
if (missed > 0) {
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
}
@@ -71,7 +104,12 @@ void GfxRenderer::ensureSdCardFontReady(int fontId, const std::vector<std::strin
// Augment the persistent advance-only table for layout measurement.
// The table survives across paragraphs/sections (capped per font), so
// repeated indexing of the same SD font amortizes glyph-metric SD reads.
int missed = it->second->buildAdvanceTable(words, includeHyphen, styleMask);
std::string shaped;
for (const auto& w : words) {
appendShapedRtlTokens(w.c_str(), shaped);
}
int missed =
it->second->buildAdvanceTable(words, includeHyphen, styleMask, shaped.empty() ? nullptr : shaped.c_str());
if (missed > 0) {
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
}
@@ -92,19 +130,46 @@ void GfxRenderer::begin() {
}
void GfxRenderer::releaseFrameBufferForBuild() {
display.releaseFrameBuffers();
// Lend the framebuffer's bytes IN PLACE: the allocation is never freed, so
// it cannot move and repeated loans cannot fragment the heap (the previous
// free+realloc model measurably decayed the max contiguous block over a
// session). The bytes are deposited in the build-scratch registry so
// memory-hungry build phases (e.g. InflateStream's tinfl state + window)
// can claim them instead of allocating.
uint32_t size = 0;
uint8_t* scratch = display.lendFrameBufferStorage(&size);
frameBuffer = nullptr;
if (scratch) {
buildscratch::lend(scratch, size);
}
}
bool GfxRenderer::restoreFrameBufferAfterBuild() {
if (!display.reallocFrameBuffers()) {
LOG_ERR("GFX", "Framebuffer realloc failed after build");
return false;
}
buildscratch::reclaim();
display.returnFrameBufferStorage(); // cannot fail: the allocation was never freed
frameBuffer = display.getFrameBuffer();
return frameBuffer != nullptr;
}
GfxRenderer::FrameBufferLoan::FrameBufferLoan(GfxRenderer& renderer) : renderer_(renderer) {
// Nesting guard: if the framebuffer is already lent out (an outer loan),
// stay inert so this end() cannot return storage the outer loan still owns.
if (!renderer_.hasFrameBuffer()) return;
renderer_.releaseFrameBufferForBuild();
active_ = true;
}
void GfxRenderer::FrameBufferLoan::end() {
if (!active_) return;
active_ = false;
if (!renderer_.restoreFrameBufferAfterBuild()) {
// Only reachable if the framebuffer never existed, which begin() already
// asserts against; kept as a backstop since running blind helps nobody.
LOG_ERR("GFX", "Framebuffer restore failed - restarting");
ESP.restart();
}
}
bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); }
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
@@ -432,18 +497,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
uint32_t cp;
uint32_t prevCp = 0;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) {
// Skip Hebrew Niqqud (vowel marks)
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
if (cp >= 0x0591 && cp <= 0x05C7) {
continue;
}
if (utf8IsCombiningMark(cp)) {
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
// niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
// marks stay centered, raised above the base or (kasra) at their
// font-native position. Fonts without their glyphs — the built-ins — miss
// the getGlyph lookup and skip them, as before.
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
if (!combiningGlyph) continue;
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop);
const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left,
combiningGlyph->width);
const auto anchor = combiningMark::anchorFor(cp);
const int raiseBy =
combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
const int combiningX = combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
combiningGlyph->left, combiningGlyph->width);
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style);
continue;
}
@@ -1629,6 +1697,15 @@ int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint3
}
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const {
// Measure the exact codepoint stream drawText renders: bidi-reordered and
// Arabic-shaped (contextual presentation forms, Lam-Alef collapse).
// Measuring the raw logical text counts the Alef a ligature absorbs and
// uses base-letter advances instead of presentation-form advances, so RTL
// lines come out wider than they draw — uneven word gaps and a ragged
// right margin.
std::string visual;
text = resolveVisualText(text, visual, BidiUtils::BidiBaseDir::AUTO);
// Advance table fast-path for SD card fonts during layout.
// No kerning/ligature lookup — consistent with previous metadataOnly behavior
// where kern/lig data was not loaded.
@@ -1644,6 +1721,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
}
const auto& font = fontIt->second;
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
if (BidiUtils::isTransparentMark(cp)) {
continue;
}
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
if (advFP == 0 && !utf8IsCombiningMark(cp)) {
const EpdGlyph* glyph = font.getGlyph(cp, style);
@@ -1666,6 +1747,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
const auto& font = fontIt->second;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
if (BidiUtils::isTransparentMark(cp)) {
continue;
}
if (utf8IsCombiningMark(cp)) {
continue;
}
@@ -1742,18 +1827,21 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
uint32_t cp;
uint32_t prevCp = 0;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
// Skip Hebrew Niqqud (vowel marks)
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
if (cp >= 0x0591 && cp <= 0x05C7) {
continue;
}
if (utf8IsCombiningMark(cp)) {
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
// niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
// marks stay centered, raised above the base or (kasra) at their
// font-native position. Fonts without their glyphs — the built-ins — miss
// the getGlyph lookup and skip them, as before.
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
if (!combiningGlyph) continue;
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop);
const auto anchor = combiningMark::anchorFor(cp);
const int raiseBy =
combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
const int combiningX = x - raiseBy;
const int combiningY = combiningMark::centerOverRotated90CW(lastBaseY, lastBaseLeft, lastBaseWidth,
const int combiningY = combiningMark::anchorOverRotated90CW(anchor, lastBaseY, lastBaseLeft, lastBaseWidth,
combiningGlyph->left, combiningGlyph->width);
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
continue;
+24 -3
View File
@@ -250,13 +250,34 @@ class GfxRenderer {
// Font helpers
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
// Lend the framebuffer to memory-hungry phases such as section pagination.
// Nothing may draw/display while it is released. restore returns the buffer
// white, so callers must redraw the full screen afterward.
// Lend the 48 KB framebuffer's bytes to a memory-hungry phase (chapter
// builds) WITHOUT freeing the allocation, so it never moves and repeated
// loans cannot fragment the heap. Between release and restore NOTHING may
// draw or display — the panel keeps showing its last refreshed image. The
// lent bytes are published via buildscratch::claim() for consumers like
// InflateStream. restore returns the buffer white, so the caller must
// redraw the full screen; it cannot fail (no allocation involved).
void releaseFrameBufferForBuild();
bool restoreFrameBufferAfterBuild();
bool hasFrameBuffer() const { return frameBuffer != nullptr; }
// RAII form of the loan above, for blocking build regions with early-return
// error paths: restores on scope exit (or explicitly via end()). Display the
// popup/screen the panel should hold BEFORE constructing one. Constructing
// while the framebuffer is already lent yields an inert loan (nesting-safe).
class FrameBufferLoan {
public:
explicit FrameBufferLoan(GfxRenderer& renderer);
~FrameBufferLoan() { end(); }
void end();
FrameBufferLoan(const FrameBufferLoan&) = delete;
FrameBufferLoan& operator=(const FrameBufferLoan&) = delete;
private:
GfxRenderer& renderer_;
bool active_ = false;
};
// Low level functions
uint8_t* getFrameBuffer() const;
size_t getBufferSize() const;