Merge remote-tracking branch 'origin/develop' into feat-bluetooth
This commit is contained in:
+114
-26
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user