From 3a0641889f6ea6a1d263da556d93d7cfa0185922 Mon Sep 17 00:00:00 2001 From: cottongin Date: Thu, 19 Feb 2026 22:20:44 -0500 Subject: [PATCH] perf: Port upstream font drawing performance optimization (PR #978) Cherry-pick upstream commit 07d715e which refactors renderChar and drawTextRotated90CW into a template-based renderCharImpl, hoisting the is2Bit branch outside inner pixel loops for 15-23% speedup. Additionally extends the template with Rotated90CCW to fix two bugs in the mod's drawTextRotated90CCW: operator precedence in bmpVal calculation and missing compressed font support via getGlyphBitmap. Co-authored-by: Cursor --- lib/GfxRenderer/GfxRenderer.cpp | 329 ++++++++++++++------------------ lib/GfxRenderer/GfxRenderer.h | 6 +- 2 files changed, 142 insertions(+), 193 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 52a5a7a7..61cea685 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -59,6 +59,130 @@ static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, } } +enum class TextRotation { None, Rotated90CW, Rotated90CCW }; + +// Shared glyph rendering logic for normal and rotated text. +// Coordinate mapping and cursor advance direction are selected at compile time via the template parameter. +template +static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode renderMode, + const EpdFontFamily& fontFamily, const uint32_t cp, int* cursorX, int* cursorY, + const bool pixelState, const EpdFontFamily::Style style) { + const EpdGlyph* glyph = fontFamily.getGlyph(cp, style); + if (!glyph) { + glyph = fontFamily.getGlyph(REPLACEMENT_GLYPH, style); + } + + if (!glyph) { + LOG_ERR("GFX", "No glyph for codepoint %d", cp); + return; + } + + const EpdFontData* fontData = fontFamily.getData(style); + const bool is2Bit = fontData->is2Bit; + const uint8_t width = glyph->width; + const uint8_t height = glyph->height; + const int left = glyph->left; + const int top = glyph->top; + + const uint8_t* bitmap = renderer.getGlyphBitmap(fontData, glyph); + + if (bitmap != nullptr) { + int outerBase, innerBase; + if constexpr (rotation == TextRotation::Rotated90CW) { + outerBase = *cursorX + fontData->ascender - top; // screenX = outerBase + glyphY + innerBase = *cursorY - left; // screenY = innerBase - glyphX + } else if constexpr (rotation == TextRotation::Rotated90CCW) { + outerBase = *cursorX + fontData->advanceY - 1 - fontData->ascender + top; // screenX = outerBase - glyphY + innerBase = *cursorY + left; // screenY = innerBase + glyphX + } else { + outerBase = *cursorY - top; // screenY = outerBase + glyphY + innerBase = *cursorX + left; // screenX = innerBase + glyphX + } + + if (is2Bit) { + int pixelPosition = 0; + for (int glyphY = 0; glyphY < height; glyphY++) { + int outerCoord; + if constexpr (rotation == TextRotation::Rotated90CCW) { + outerCoord = outerBase - glyphY; + } else { + outerCoord = outerBase + glyphY; + } + for (int glyphX = 0; glyphX < width; glyphX++, pixelPosition++) { + int screenX, screenY; + if constexpr (rotation == TextRotation::Rotated90CW) { + screenX = outerCoord; + screenY = innerBase - glyphX; + } else if constexpr (rotation == TextRotation::Rotated90CCW) { + screenX = outerCoord; + screenY = innerBase + glyphX; + } else { + screenX = innerBase + glyphX; + screenY = outerCoord; + } + + const uint8_t byte = bitmap[pixelPosition >> 2]; + const uint8_t bit_index = (3 - (pixelPosition & 3)) * 2; + // the direct bit from the font is 0 -> white, 1 -> light gray, 2 -> dark gray, 3 -> black + // we swap this to better match the way images and screen think about colors: + // 0 -> black, 1 -> dark grey, 2 -> light grey, 3 -> white + const uint8_t bmpVal = 3 - ((byte >> bit_index) & 0x3); + + if (renderMode == GfxRenderer::BW && bmpVal < 3) { + // Black (also paints over the grays in BW mode) + renderer.drawPixel(screenX, screenY, pixelState); + } else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { + // Light gray (also mark the MSB if it's going to be a dark gray too) + // We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update + renderer.drawPixel(screenX, screenY, false); + } else if (renderMode == GfxRenderer::GRAYSCALE_LSB && bmpVal == 1) { + // Dark gray + renderer.drawPixel(screenX, screenY, false); + } + } + } + } else { + int pixelPosition = 0; + for (int glyphY = 0; glyphY < height; glyphY++) { + int outerCoord; + if constexpr (rotation == TextRotation::Rotated90CCW) { + outerCoord = outerBase - glyphY; + } else { + outerCoord = outerBase + glyphY; + } + for (int glyphX = 0; glyphX < width; glyphX++, pixelPosition++) { + int screenX, screenY; + if constexpr (rotation == TextRotation::Rotated90CW) { + screenX = outerCoord; + screenY = innerBase - glyphX; + } else if constexpr (rotation == TextRotation::Rotated90CCW) { + screenX = outerCoord; + screenY = innerBase + glyphX; + } else { + screenX = innerBase + glyphX; + screenY = outerCoord; + } + + const uint8_t byte = bitmap[pixelPosition >> 3]; + const uint8_t bit_index = 7 - (pixelPosition & 7); + + if ((byte >> bit_index) & 1) { + renderer.drawPixel(screenX, screenY, pixelState); + } + } + } + } + } + + if constexpr (rotation == TextRotation::Rotated90CW) { + *cursorY -= glyph->advanceX; + } else if constexpr (rotation == TextRotation::Rotated90CCW) { + *cursorY += glyph->advanceX; + } else { + *cursorX += glyph->advanceX; + } +} + // IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and // efficient as possible. void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { @@ -115,7 +239,7 @@ void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* te void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black, const EpdFontFamily::Style style) const { - const int yPos = y + getFontAscenderSize(fontId); + int yPos = y + getFontAscenderSize(fontId); int xpos = x; // cannot draw a NULL / empty string @@ -890,68 +1014,12 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y const auto& font = fontIt->second; - // For 90° clockwise rotation: - // Original (glyphX, glyphY) -> Rotated (glyphY, -glyphX) - // Text reads from bottom to top - - int yPos = y; // Current Y position (decreases as we draw characters) + int xPos = x; + int yPos = y; uint32_t cp; while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { - const EpdGlyph* glyph = font.getGlyph(cp, style); - if (!glyph) { - glyph = font.getGlyph(REPLACEMENT_GLYPH, style); - } - if (!glyph) { - continue; - } - - const EpdFontData* fontData = font.getData(style); - const int is2Bit = fontData->is2Bit; - const uint8_t width = glyph->width; - const uint8_t height = glyph->height; - const int left = glyph->left; - const int top = glyph->top; - - const uint8_t* bitmap = getGlyphBitmap(fontData, glyph); - - if (bitmap != nullptr) { - for (int glyphY = 0; glyphY < height; glyphY++) { - for (int glyphX = 0; glyphX < width; glyphX++) { - const int pixelPosition = glyphY * width + glyphX; - - // 90° clockwise rotation transformation: - // screenX = x + (ascender - top + glyphY) - // screenY = yPos - (left + glyphX) - const int screenX = x + (fontData->ascender - top + glyphY); - const int screenY = yPos - left - glyphX; - - if (is2Bit) { - const uint8_t byte = bitmap[pixelPosition / 4]; - const uint8_t bit_index = (3 - pixelPosition % 4) * 2; - const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3; - - if (renderMode == BW && bmpVal < 3) { - drawPixel(screenX, screenY, black); - } else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { - drawPixel(screenX, screenY, false); - } else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) { - drawPixel(screenX, screenY, false); - } - } else { - const uint8_t byte = bitmap[pixelPosition / 8]; - const uint8_t bit_index = 7 - (pixelPosition % 8); - - if ((byte >> bit_index) & 1) { - drawPixel(screenX, screenY, black); - } - } - } - } - } - - // Move to next character position (going up, so decrease Y) - yPos -= glyph->advanceX; + renderCharImpl(*this, renderMode, font, cp, &xPos, &yPos, black, style); } } @@ -962,77 +1030,20 @@ void GfxRenderer::drawTextRotated90CCW(const int fontId, const int x, const int return; } - if (fontMap.count(fontId) == 0) { + const auto fontIt = fontMap.find(fontId); + if (fontIt == fontMap.end()) { LOG_ERR("GFX", "Font %d not found", fontId); return; } - const auto font = fontMap.at(fontId); - // For 90° counter-clockwise rotation: - // Mirror of CW: glyphY maps to -X direction, glyphX maps to +Y direction - // Text reads from top to bottom + const auto& font = fontIt->second; - const int advanceY = font.getData(style)->advanceY; - const int ascender = font.getData(style)->ascender; - - int yPos = y; // Current Y position (increases as we draw characters) + int xPos = x; + int yPos = y; uint32_t cp; while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { - const EpdGlyph* glyph = font.getGlyph(cp, style); - if (!glyph) { - glyph = font.getGlyph(REPLACEMENT_GLYPH, style); - } - if (!glyph) { - continue; - } - - const int is2Bit = font.getData(style)->is2Bit; - const uint32_t offset = glyph->dataOffset; - const uint8_t width = glyph->width; - const uint8_t height = glyph->height; - const int left = glyph->left; - const int top = glyph->top; - - const uint8_t* bitmap = &font.getData(style)->bitmap[offset]; - - if (bitmap != nullptr) { - for (int glyphY = 0; glyphY < height; glyphY++) { - for (int glyphX = 0; glyphX < width; glyphX++) { - const int pixelPosition = glyphY * width + glyphX; - - // 90° counter-clockwise rotation transformation: - // screenX = mirrored CW X (right-to-left within advanceY span) - // screenY = yPos + (left + glyphX) (downward) - const int screenX = x + advanceY - 1 - (ascender - top + glyphY); - const int screenY = yPos + left + glyphX; - - if (is2Bit) { - const uint8_t byte = bitmap[pixelPosition / 4]; - const uint8_t bit_index = (3 - pixelPosition % 4) * 2; - const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3; - - if (renderMode == BW && bmpVal < 3) { - drawPixel(screenX, screenY, black); - } else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { - drawPixel(screenX, screenY, false); - } else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) { - drawPixel(screenX, screenY, false); - } - } else { - const uint8_t byte = bitmap[pixelPosition / 8]; - const uint8_t bit_index = 7 - (pixelPosition % 8); - - if ((byte >> bit_index) & 1) { - drawPixel(screenX, screenY, black); - } - } - } - } - } - - // Move to next character position (going down, so increase Y) - yPos += glyph->advanceX; + renderCharImpl(*this, renderMode, font, cp, &xPos, &yPos, black, style); } } @@ -1097,7 +1108,7 @@ bool GfxRenderer::storeBwBuffer() { * Uses chunked restoration to match chunked storage. */ void GfxRenderer::restoreBwBuffer() { - // Check if any all chunks are allocated + // Check if all chunks are allocated bool missingChunks = false; for (const auto& bwBufferChunk : bwBufferChunks) { if (!bwBufferChunk) { @@ -1112,13 +1123,6 @@ void GfxRenderer::restoreBwBuffer() { } for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) { - // Check if chunk is missing - if (!bwBufferChunks[i]) { - LOG_ERR("GFX", "!! BW buffer chunks not stored - this is likely a bug"); - freeBwBufferChunks(); - return; - } - const size_t offset = i * BW_BUFFER_CHUNK_SIZE; memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE); } @@ -1139,66 +1143,9 @@ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const { } } -void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y, - const bool pixelState, const EpdFontFamily::Style style) const { - const EpdGlyph* glyph = fontFamily.getGlyph(cp, style); - if (!glyph) { - glyph = fontFamily.getGlyph(REPLACEMENT_GLYPH, style); - } - - // no glyph? - if (!glyph) { - LOG_ERR("GFX", "No glyph for codepoint %d", cp); - return; - } - - const EpdFontData* fontData = fontFamily.getData(style); - const int is2Bit = fontData->is2Bit; - const uint8_t width = glyph->width; - const uint8_t height = glyph->height; - const int left = glyph->left; - - const uint8_t* bitmap = getGlyphBitmap(fontData, glyph); - - if (bitmap != nullptr) { - for (int glyphY = 0; glyphY < height; glyphY++) { - const int screenY = *y - glyph->top + glyphY; - for (int glyphX = 0; glyphX < width; glyphX++) { - const int pixelPosition = glyphY * width + glyphX; - const int screenX = *x + left + glyphX; - - if (is2Bit) { - const uint8_t byte = bitmap[pixelPosition / 4]; - const uint8_t bit_index = (3 - pixelPosition % 4) * 2; - // the direct bit from the font is 0 -> white, 1 -> light gray, 2 -> dark gray, 3 -> black - // we swap this to better match the way images and screen think about colors: - // 0 -> black, 1 -> dark grey, 2 -> light grey, 3 -> white - const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3; - - if (renderMode == BW && bmpVal < 3) { - // Black (also paints over the grays in BW mode) - drawPixel(screenX, screenY, pixelState); - } else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { - // Light gray (also mark the MSB if it's going to be a dark gray too) - // We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update - drawPixel(screenX, screenY, false); - } else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) { - // Dark gray - drawPixel(screenX, screenY, false); - } - } else { - const uint8_t byte = bitmap[pixelPosition / 8]; - const uint8_t bit_index = 7 - (pixelPosition % 8); - - if ((byte >> bit_index) & 1) { - drawPixel(screenX, screenY, pixelState); - } - } - } - } - } - - *x += glyph->advanceX; +void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState, + EpdFontFamily::Style style) const { + renderCharImpl(*this, renderMode, fontFamily, cp, x, y, pixelState, style); } void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const { diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index bd3227d0..c694582e 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -38,10 +38,9 @@ class GfxRenderer { uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; std::map fontMap; FontDecompressor* fontDecompressor = nullptr; - void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState, + void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState, EpdFontFamily::Style style) const; void freeBwBufferChunks(); - const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const; template void drawPixelDither(int x, int y) const; template @@ -136,6 +135,9 @@ class GfxRenderer { void restoreBwBuffer(); // Restore and free the stored buffer void cleanupGrayscaleWithFrameBuffer() const; + // Font helpers + const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const; + // Low level functions uint8_t* getFrameBuffer() const; static size_t getBufferSize();