A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at records-reader 1289 lines 46 kB view raw
1#include "GfxRenderer.h" 2 3#include <FontDecompressor.h> 4#include <HalGPIO.h> 5#include <Logging.h> 6#include <Utf8.h> 7 8#include "FontCacheManager.h" 9 10const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const { 11 if (fontData->groups != nullptr) { 12 auto* fd = fontCacheManager_ ? fontCacheManager_->getDecompressor() : nullptr; 13 if (!fd) { 14 LOG_ERR("GFX", "Compressed font but no FontDecompressor set"); 15 return nullptr; 16 } 17 uint32_t glyphIndex = static_cast<uint32_t>(glyph - fontData->glyph); 18 // For page-buffer hits the pointer is stable for the page lifetime. 19 // For hot-group hits it is valid only until the next getBitmap() call — callers 20 // must consume it (draw the glyph) before requesting another bitmap. 21 return fd->getBitmap(fontData, glyph, glyphIndex); 22 } 23 return &fontData->bitmap[glyph->dataOffset]; 24} 25 26void GfxRenderer::begin() { 27 frameBuffer = display.getFrameBuffer(); 28 if (!frameBuffer) { 29 LOG_ERR("GFX", "!! No framebuffer"); 30 assert(false); 31 } 32 panelWidth = display.getDisplayWidth(); 33 panelHeight = display.getDisplayHeight(); 34 panelWidthBytes = display.getDisplayWidthBytes(); 35 frameBufferSize = display.getBufferSize(); 36 bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr); 37} 38 39void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); } 40 41// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation 42// This should always be inlined for better performance 43static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX, 44 int* phyY, const uint16_t panelWidth, const uint16_t panelHeight) { 45 switch (orientation) { 46 case GfxRenderer::Portrait: { 47 // Logical portrait (480x800) → panel (800x480) 48 // Rotation: 90 degrees clockwise 49 *phyX = y; 50 *phyY = panelHeight - 1 - x; 51 break; 52 } 53 case GfxRenderer::LandscapeClockwise: { 54 // Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right) 55 *phyX = panelWidth - 1 - x; 56 *phyY = panelHeight - 1 - y; 57 break; 58 } 59 case GfxRenderer::PortraitInverted: { 60 // Logical portrait (480x800) → panel (800x480) 61 // Rotation: 90 degrees counter-clockwise 62 *phyX = panelWidth - 1 - y; 63 *phyY = x; 64 break; 65 } 66 case GfxRenderer::LandscapeCounterClockwise: { 67 // Logical landscape (800x480) aligned with panel orientation 68 *phyX = x; 69 *phyY = y; 70 break; 71 } 72 } 73} 74 75enum class TextRotation { None, Rotated90CW }; 76 77// Shared glyph rendering logic for normal and rotated text. 78// Coordinate mapping and cursor advance direction are selected at compile time via the template parameter. 79template <TextRotation rotation> 80static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode renderMode, 81 const EpdFontFamily& fontFamily, const uint32_t cp, int cursorX, int cursorY, 82 const bool pixelState, const EpdFontFamily::Style style) { 83 const EpdGlyph* glyph = fontFamily.getGlyph(cp, style); 84 if (!glyph) { 85 LOG_ERR("GFX", "No glyph for codepoint %d", cp); 86 return; 87 } 88 89 const EpdFontData* fontData = fontFamily.getData(style); 90 const bool is2Bit = fontData->is2Bit; 91 const uint8_t width = glyph->width; 92 const uint8_t height = glyph->height; 93 const int left = glyph->left; 94 const int top = glyph->top; 95 96 const uint8_t* bitmap = renderer.getGlyphBitmap(fontData, glyph); 97 98 if (bitmap != nullptr) { 99 // For Normal: outer loop advances screenY, inner loop advances screenX 100 // For Rotated: outer loop advances screenX, inner loop advances screenY (in reverse) 101 int outerBase, innerBase; 102 if constexpr (rotation == TextRotation::Rotated90CW) { 103 outerBase = cursorX + fontData->ascender - top; // screenX = outerBase + glyphY 104 innerBase = cursorY - left; // screenY = innerBase - glyphX 105 } else { 106 outerBase = cursorY - top; // screenY = outerBase + glyphY 107 innerBase = cursorX + left; // screenX = innerBase + glyphX 108 } 109 110 if (is2Bit) { 111 int pixelPosition = 0; 112 for (int glyphY = 0; glyphY < height; glyphY++) { 113 const int outerCoord = outerBase + glyphY; 114 for (int glyphX = 0; glyphX < width; glyphX++, pixelPosition++) { 115 int screenX, screenY; 116 if constexpr (rotation == TextRotation::Rotated90CW) { 117 screenX = outerCoord; 118 screenY = innerBase - glyphX; 119 } else { 120 screenX = innerBase + glyphX; 121 screenY = outerCoord; 122 } 123 124 const uint8_t byte = bitmap[pixelPosition >> 2]; 125 const uint8_t bit_index = (3 - (pixelPosition & 3)) * 2; 126 // the direct bit from the font is 0 -> white, 1 -> light gray, 2 -> dark gray, 3 -> black 127 // we swap this to better match the way images and screen think about colors: 128 // 0 -> black, 1 -> dark grey, 2 -> light grey, 3 -> white 129 const uint8_t bmpVal = 3 - ((byte >> bit_index) & 0x3); 130 131 if (renderMode == GfxRenderer::BW && bmpVal < 3) { 132 // Black (also paints over the grays in BW mode) 133 renderer.drawPixel(screenX, screenY, pixelState); 134 } else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { 135 // Light gray (also mark the MSB if it's going to be a dark gray too) 136 // Dedicated X3 gray LUTs now provide proper 4-level gray on both devices 137 // We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update 138 renderer.drawPixel(screenX, screenY, false); 139 } else if (renderMode == GfxRenderer::GRAYSCALE_LSB && bmpVal == 1) { 140 // Dark gray 141 renderer.drawPixel(screenX, screenY, false); 142 } 143 } 144 } 145 } else { 146 int pixelPosition = 0; 147 for (int glyphY = 0; glyphY < height; glyphY++) { 148 const int outerCoord = outerBase + glyphY; 149 for (int glyphX = 0; glyphX < width; glyphX++, pixelPosition++) { 150 int screenX, screenY; 151 if constexpr (rotation == TextRotation::Rotated90CW) { 152 screenX = outerCoord; 153 screenY = innerBase - glyphX; 154 } else { 155 screenX = innerBase + glyphX; 156 screenY = outerCoord; 157 } 158 159 const uint8_t byte = bitmap[pixelPosition >> 3]; 160 const uint8_t bit_index = 7 - (pixelPosition & 7); 161 162 if ((byte >> bit_index) & 1) { 163 renderer.drawPixel(screenX, screenY, pixelState); 164 } 165 } 166 } 167 } 168 } 169} 170 171// IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and 172// efficient as possible. 173void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { 174 int phyX = 0; 175 int phyY = 0; 176 177 // Note: this call should be inlined for better performance 178 rotateCoordinates(orientation, x, y, &phyX, &phyY, panelWidth, panelHeight); 179 180 // Bounds checking against runtime panel dimensions 181 if (phyX < 0 || phyX >= panelWidth || phyY < 0 || phyY >= panelHeight) { 182 LOG_ERR("GFX", "!! Outside range (%d, %d) -> (%d, %d)", x, y, phyX, phyY); 183 return; 184 } 185 186 // Calculate byte position and bit position 187 const uint32_t byteIndex = static_cast<uint32_t>(phyY) * panelWidthBytes + (phyX / 8); 188 const uint8_t bitPosition = 7 - (phyX % 8); // MSB first 189 190 if (state) { 191 frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit 192 } else { 193 frameBuffer[byteIndex] |= 1 << bitPosition; // Set bit 194 } 195} 196 197int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const { 198 const auto fontIt = fontMap.find(fontId); 199 if (fontIt == fontMap.end()) { 200 LOG_ERR("GFX", "Font %d not found", fontId); 201 return 0; 202 } 203 204 int w = 0, h = 0; 205 fontIt->second.getTextDimensions(text, &w, &h, style); 206 return w; 207} 208 209void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* text, const bool black, 210 const EpdFontFamily::Style style) const { 211 const int x = (getScreenWidth() - getTextWidth(fontId, text, style)) / 2; 212 drawText(fontId, x, y, text, black, style); 213} 214 215void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black, 216 const EpdFontFamily::Style style) const { 217 const int yPos = y + getFontAscenderSize(fontId); 218 int lastBaseX = x; 219 int lastBaseLeft = 0; 220 int lastBaseWidth = 0; 221 int lastBaseTop = 0; 222 int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap 223 224 // cannot draw a NULL / empty string 225 if (text == nullptr || *text == '\0') { 226 return; 227 } 228 229 if (fontCacheManager_ && fontCacheManager_->isScanning()) { 230 fontCacheManager_->recordText(text, fontId, style); 231 return; 232 } 233 234 const auto fontIt = fontMap.find(fontId); 235 if (fontIt == fontMap.end()) { 236 LOG_ERR("GFX", "Font %d not found", fontId); 237 return; 238 } 239 const auto& font = fontIt->second; 240 241 uint32_t cp; 242 uint32_t prevCp = 0; 243 while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) { 244 if (utf8IsCombiningMark(cp)) { 245 const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); 246 if (!combiningGlyph) continue; 247 const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop); 248 const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left, 249 combiningGlyph->width); 250 renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style); 251 continue; 252 } 253 254 cp = font.applyLigatures(cp, text, style); 255 256 // Differential rounding: snap (previous advance + current kern) as one unit so 257 // identical character pairs always produce the same pixel step regardless of 258 // where they fall on the line. 259 if (prevCp != 0) { 260 const auto kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern 261 lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel 262 } 263 264 const EpdGlyph* glyph = font.getGlyph(cp, style); 265 266 lastBaseLeft = glyph ? glyph->left : 0; 267 lastBaseWidth = glyph ? glyph->width : 0; 268 lastBaseTop = glyph ? glyph->top : 0; 269 prevAdvanceFP = glyph ? glyph->advanceX : 0; // 12.4 fixed-point 270 271 renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, lastBaseX, yPos, black, style); 272 prevCp = cp; 273 } 274} 275 276void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const { 277 if (fontCacheManager_ && fontCacheManager_->isScanning()) return; 278 if (x1 == x2) { 279 if (y2 < y1) { 280 std::swap(y1, y2); 281 } 282 for (int y = y1; y <= y2; y++) { 283 drawPixel(x1, y, state); 284 } 285 } else if (y1 == y2) { 286 if (x2 < x1) { 287 std::swap(x1, x2); 288 } 289 for (int x = x1; x <= x2; x++) { 290 drawPixel(x, y1, state); 291 } 292 } else { 293 // Bresenham's line algorithm — integer arithmetic only 294 int dx = x2 - x1; 295 int dy = y2 - y1; 296 int sx = (dx > 0) ? 1 : -1; 297 int sy = (dy > 0) ? 1 : -1; 298 dx = sx * dx; // abs 299 dy = sy * dy; // abs 300 301 int err = dx - dy; 302 while (true) { 303 drawPixel(x1, y1, state); 304 if (x1 == x2 && y1 == y2) break; 305 int e2 = 2 * err; 306 if (e2 > -dy) { 307 err -= dy; 308 x1 += sx; 309 } 310 if (e2 < dx) { 311 err += dx; 312 y1 += sy; 313 } 314 } 315 } 316} 317 318void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const int lineWidth, const bool state) const { 319 for (int i = 0; i < lineWidth; i++) { 320 drawLine(x1, y1 + i, x2, y2 + i, state); 321 } 322} 323 324void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const { 325 drawLine(x, y, x + width - 1, y, state); 326 drawLine(x + width - 1, y, x + width - 1, y + height - 1, state); 327 drawLine(x + width - 1, y + height - 1, x, y + height - 1, state); 328 drawLine(x, y, x, y + height - 1, state); 329} 330 331// Border is inside the rectangle 332void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const int lineWidth, 333 const bool state) const { 334 for (int i = 0; i < lineWidth; i++) { 335 drawLine(x + i, y + i, x + width - i, y + i, state); 336 drawLine(x + width - i, y + i, x + width - i, y + height - i, state); 337 drawLine(x + width - i, y + height - i, x + i, y + height - i, state); 338 drawLine(x + i, y + height - i, x + i, y + i, state); 339 } 340} 341 342void GfxRenderer::drawArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir, 343 const int lineWidth, const bool state) const { 344 const int stroke = std::min(lineWidth, maxRadius); 345 const int innerRadius = std::max(maxRadius - stroke, 0); 346 const int outerRadius = maxRadius; 347 348 if (outerRadius <= 0) { 349 return; 350 } 351 352 const int outerRadiusSq = outerRadius * outerRadius; 353 const int innerRadiusSq = innerRadius * innerRadius; 354 355 int xOuter = outerRadius; 356 int xInner = innerRadius; 357 358 for (int dy = 0; dy <= outerRadius; ++dy) { 359 while (xOuter > 0 && (xOuter * xOuter + dy * dy) > outerRadiusSq) { 360 --xOuter; 361 } 362 // Keep the smallest x that still lies outside/at the inner radius, 363 // i.e. (x^2 + y^2) >= innerRadiusSq. 364 while (xInner > 0 && ((xInner - 1) * (xInner - 1) + dy * dy) >= innerRadiusSq) { 365 --xInner; 366 } 367 368 if (xOuter < xInner) { 369 continue; 370 } 371 372 const int x0 = cx + xDir * xInner; 373 const int x1 = cx + xDir * xOuter; 374 const int left = std::min(x0, x1); 375 const int width = std::abs(x1 - x0) + 1; 376 const int py = cy + yDir * dy; 377 378 if (width > 0) { 379 fillRect(left, py, width, 1, state); 380 } 381 } 382}; 383 384// Border is inside the rectangle, rounded corners 385void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, const int height, const int lineWidth, 386 const int cornerRadius, bool state) const { 387 drawRoundedRect(x, y, width, height, lineWidth, cornerRadius, true, true, true, true, state); 388} 389 390// Border is inside the rectangle, rounded corners 391void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, const int height, const int lineWidth, 392 const int cornerRadius, bool roundTopLeft, bool roundTopRight, bool roundBottomLeft, 393 bool roundBottomRight, bool state) const { 394 if (lineWidth <= 0 || width <= 0 || height <= 0) { 395 return; 396 } 397 398 const int maxRadius = std::min({cornerRadius, width / 2, height / 2}); 399 if (maxRadius <= 0) { 400 drawRect(x, y, width, height, lineWidth, state); 401 return; 402 } 403 404 const int stroke = std::min(lineWidth, maxRadius); 405 const int right = x + width - 1; 406 const int bottom = y + height - 1; 407 408 const int horizontalWidth = width - 2 * maxRadius; 409 if (horizontalWidth > 0) { 410 if (roundTopLeft || roundTopRight) { 411 fillRect(x + maxRadius, y, horizontalWidth, stroke, state); 412 } 413 if (roundBottomLeft || roundBottomRight) { 414 fillRect(x + maxRadius, bottom - stroke + 1, horizontalWidth, stroke, state); 415 } 416 } 417 418 const int verticalHeight = height - 2 * maxRadius; 419 if (verticalHeight > 0) { 420 if (roundTopLeft || roundBottomLeft) { 421 fillRect(x, y + maxRadius, stroke, verticalHeight, state); 422 } 423 if (roundTopRight || roundBottomRight) { 424 fillRect(right - stroke + 1, y + maxRadius, stroke, verticalHeight, state); 425 } 426 } 427 428 if (roundTopLeft) { 429 drawArc(maxRadius, x + maxRadius, y + maxRadius, -1, -1, lineWidth, state); 430 } 431 if (roundTopRight) { 432 drawArc(maxRadius, right - maxRadius, y + maxRadius, 1, -1, lineWidth, state); 433 } 434 if (roundBottomRight) { 435 drawArc(maxRadius, right - maxRadius, bottom - maxRadius, 1, 1, lineWidth, state); 436 } 437 if (roundBottomLeft) { 438 drawArc(maxRadius, x + maxRadius, bottom - maxRadius, -1, 1, lineWidth, state); 439 } 440} 441 442void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const { 443 for (int fillY = y; fillY < y + height; fillY++) { 444 drawLine(x, fillY, x + width - 1, fillY, state); 445 } 446} 447 448// NOTE: Those are in critical path, and need to be templated to avoid runtime checks for every pixel. 449// Any branching must be done outside the loops to avoid performance degradation. 450template <> 451void GfxRenderer::drawPixelDither<Color::Clear>(const int x, const int y) const { 452 // Do nothing 453} 454 455template <> 456void GfxRenderer::drawPixelDither<Color::Black>(const int x, const int y) const { 457 drawPixel(x, y, true); 458} 459 460template <> 461void GfxRenderer::drawPixelDither<Color::White>(const int x, const int y) const { 462 drawPixel(x, y, false); 463} 464 465template <> 466void GfxRenderer::drawPixelDither<Color::LightGray>(const int x, const int y) const { 467 drawPixel(x, y, x % 2 == 0 && y % 2 == 0); 468} 469 470template <> 471void GfxRenderer::drawPixelDither<Color::DarkGray>(const int x, const int y) const { 472 drawPixel(x, y, (x + y) % 2 == 0); // TODO: maybe find a better pattern? 473} 474 475void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const { 476 if (color == Color::Clear) { 477 } else if (color == Color::Black) { 478 fillRect(x, y, width, height, true); 479 } else if (color == Color::White) { 480 fillRect(x, y, width, height, false); 481 } else if (color == Color::LightGray) { 482 for (int fillY = y; fillY < y + height; fillY++) { 483 for (int fillX = x; fillX < x + width; fillX++) { 484 drawPixelDither<Color::LightGray>(fillX, fillY); 485 } 486 } 487 } else if (color == Color::DarkGray) { 488 for (int fillY = y; fillY < y + height; fillY++) { 489 for (int fillX = x; fillX < x + width; fillX++) { 490 drawPixelDither<Color::DarkGray>(fillX, fillY); 491 } 492 } 493 } 494} 495 496template <Color color> 497void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir) const { 498 if (maxRadius <= 0) return; 499 500 if constexpr (color == Color::Clear) { 501 return; 502 } 503 504 const int radiusSq = maxRadius * maxRadius; 505 506 // Avoid sqrt by scanning from outer radius inward while y grows. 507 int x = maxRadius; 508 for (int dy = 0; dy <= maxRadius; ++dy) { 509 while (x > 0 && (x * x + dy * dy) > radiusSq) { 510 --x; 511 } 512 if (x < 0) break; 513 514 const int py = cy + yDir * dy; 515 if (py < 0 || py >= getScreenHeight()) continue; 516 517 int x0 = cx; 518 int x1 = cx + xDir * x; 519 if (x0 > x1) std::swap(x0, x1); 520 const int width = x1 - x0 + 1; 521 522 if (width <= 0) continue; 523 524 if constexpr (color == Color::Black) { 525 fillRect(x0, py, width, 1, true); 526 } else if constexpr (color == Color::White) { 527 fillRect(x0, py, width, 1, false); 528 } else { 529 // LightGray / DarkGray: use existing dithered fill path. 530 fillRectDither(x0, py, width, 1, color); 531 } 532 } 533} 534 535void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, const int height, const int cornerRadius, 536 const Color color) const { 537 fillRoundedRect(x, y, width, height, cornerRadius, true, true, true, true, color); 538} 539 540void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, const int height, const int cornerRadius, 541 bool roundTopLeft, bool roundTopRight, bool roundBottomLeft, bool roundBottomRight, 542 const Color color) const { 543 if (width <= 0 || height <= 0) { 544 return; 545 } 546 547 // Assume if we're not rounding all corners then we are only rounding one side 548 const int roundedSides = (!roundTopLeft || !roundTopRight || !roundBottomLeft || !roundBottomRight) ? 1 : 2; 549 const int maxRadius = std::min({cornerRadius, width / roundedSides, height / roundedSides}); 550 if (maxRadius <= 0) { 551 fillRectDither(x, y, width, height, color); 552 return; 553 } 554 555 const int horizontalWidth = width - 2 * maxRadius; 556 if (horizontalWidth > 0) { 557 fillRectDither(x + maxRadius + 1, y, horizontalWidth - 2, height, color); 558 } 559 560 const int leftFillTop = y + (roundTopLeft ? (maxRadius + 1) : 0); 561 const int leftFillBottom = y + height - 1 - (roundBottomLeft ? (maxRadius + 1) : 0); 562 if (leftFillBottom >= leftFillTop) { 563 fillRectDither(x, leftFillTop, maxRadius + 1, leftFillBottom - leftFillTop + 1, color); 564 } 565 566 const int rightFillTop = y + (roundTopRight ? (maxRadius + 1) : 0); 567 const int rightFillBottom = y + height - 1 - (roundBottomRight ? (maxRadius + 1) : 0); 568 if (rightFillBottom >= rightFillTop) { 569 fillRectDither(x + width - maxRadius - 1, rightFillTop, maxRadius + 1, rightFillBottom - rightFillTop + 1, color); 570 } 571 572 auto fillArcTemplated = [this](int maxRadius, int cx, int cy, int xDir, int yDir, Color color) { 573 switch (color) { 574 case Color::Clear: 575 break; 576 case Color::Black: 577 fillArc<Color::Black>(maxRadius, cx, cy, xDir, yDir); 578 break; 579 case Color::White: 580 fillArc<Color::White>(maxRadius, cx, cy, xDir, yDir); 581 break; 582 case Color::LightGray: 583 fillArc<Color::LightGray>(maxRadius, cx, cy, xDir, yDir); 584 break; 585 case Color::DarkGray: 586 fillArc<Color::DarkGray>(maxRadius, cx, cy, xDir, yDir); 587 break; 588 } 589 }; 590 591 if (roundTopLeft) { 592 fillArcTemplated(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color); 593 } 594 595 if (roundTopRight) { 596 fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color); 597 } 598 599 if (roundBottomRight) { 600 fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color); 601 } 602 603 if (roundBottomLeft) { 604 fillArcTemplated(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color); 605 } 606} 607 608void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const { 609 int rotatedX = 0; 610 int rotatedY = 0; 611 rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY, panelWidth, panelHeight); 612 // Rotate origin corner 613 switch (orientation) { 614 case Portrait: 615 rotatedY = rotatedY - height; 616 break; 617 case PortraitInverted: 618 rotatedX = rotatedX - width; 619 break; 620 case LandscapeClockwise: 621 rotatedY = rotatedY - height; 622 rotatedX = rotatedX - width; 623 break; 624 case LandscapeCounterClockwise: 625 break; 626 } 627 // TODO: Rotate bits 628 display.drawImage(bitmap, rotatedX, rotatedY, width, height); 629} 630 631void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const { 632 display.drawImageTransparent(bitmap, y, getScreenWidth() - width - x, height, width); 633} 634 635void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight, 636 const float cropX, const float cropY) const { 637 if (fontCacheManager_ && fontCacheManager_->isScanning()) return; 638 // For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit) 639 if (bitmap.is1Bit() && cropX == 0.0f && cropY == 0.0f) { 640 drawBitmap1Bit(bitmap, x, y, maxWidth, maxHeight); 641 return; 642 } 643 644 float scale = 1.0f; 645 bool isScaled = false; 646 int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f); 647 int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f); 648 LOG_DBG("GFX", "Cropping %dx%d by %dx%d pix, is %s", bitmap.getWidth(), bitmap.getHeight(), cropPixX, cropPixY, 649 bitmap.isTopDown() ? "top-down" : "bottom-up"); 650 651 const float croppedWidth = (1.0f - cropX) * static_cast<float>(bitmap.getWidth()); 652 const float croppedHeight = (1.0f - cropY) * static_cast<float>(bitmap.getHeight()); 653 bool hasTargetBounds = false; 654 float fitScale = 1.0f; 655 656 if (maxWidth > 0 && croppedWidth > 0.0f) { 657 fitScale = static_cast<float>(maxWidth) / croppedWidth; 658 hasTargetBounds = true; 659 } 660 661 if (maxHeight > 0 && croppedHeight > 0.0f) { 662 const float heightScale = static_cast<float>(maxHeight) / croppedHeight; 663 fitScale = hasTargetBounds ? std::min(fitScale, heightScale) : heightScale; 664 hasTargetBounds = true; 665 } 666 667 if (hasTargetBounds && fitScale < 1.0f) { 668 scale = fitScale; 669 isScaled = true; 670 } 671 LOG_DBG("GFX", "Scaling by %f - %s", scale, isScaled ? "scaled" : "not scaled"); 672 673 // Calculate output row size (2 bits per pixel, packed into bytes) 674 // IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide 675 const int outputRowSize = (bitmap.getWidth() + 3) / 4; 676 auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize)); 677 auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes())); 678 679 if (!outputRow || !rowBytes) { 680 LOG_ERR("GFX", "!! Failed to allocate BMP row buffers"); 681 free(outputRow); 682 free(rowBytes); 683 return; 684 } 685 686 for (int bmpY = 0; bmpY < (bitmap.getHeight() - cropPixY); bmpY++) { 687 // The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative). 688 // Screen's (0, 0) is the top-left corner. 689 int screenY = -cropPixY + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY); 690 if (isScaled) { 691 screenY = std::floor(screenY * scale); 692 } 693 screenY += y; // the offset should not be scaled 694 if (screenY >= getScreenHeight()) { 695 break; 696 } 697 698 if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) { 699 LOG_ERR("GFX", "Failed to read row %d from bitmap", bmpY); 700 free(outputRow); 701 free(rowBytes); 702 return; 703 } 704 705 if (screenY < 0) { 706 continue; 707 } 708 709 if (bmpY < cropPixY) { 710 // Skip the row if it's outside the crop area 711 continue; 712 } 713 714 for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) { 715 int screenX = bmpX - cropPixX; 716 if (isScaled) { 717 screenX = std::floor(screenX * scale); 718 } 719 screenX += x; // the offset should not be scaled 720 if (screenX >= getScreenWidth()) { 721 break; 722 } 723 if (screenX < 0) { 724 continue; 725 } 726 727 const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3; 728 729 if (renderMode == BW && val < 3) { 730 drawPixel(screenX, screenY); 731 } else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) { 732 drawPixel(screenX, screenY, false); 733 } else if (renderMode == GRAYSCALE_LSB && val == 1) { 734 drawPixel(screenX, screenY, false); 735 } 736 } 737 } 738 739 free(outputRow); 740 free(rowBytes); 741} 742 743void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y, const int maxWidth, 744 const int maxHeight) const { 745 float scale = 1.0f; 746 bool isScaled = false; 747 if (maxWidth > 0 && bitmap.getWidth() > maxWidth) { 748 scale = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth()); 749 isScaled = true; 750 } 751 if (maxHeight > 0 && bitmap.getHeight() > maxHeight) { 752 scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight())); 753 isScaled = true; 754 } 755 756 // For 1-bit BMP, output is still 2-bit packed (for consistency with readNextRow) 757 const int outputRowSize = (bitmap.getWidth() + 3) / 4; 758 auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize)); 759 auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes())); 760 761 if (!outputRow || !rowBytes) { 762 LOG_ERR("GFX", "!! Failed to allocate 1-bit BMP row buffers"); 763 free(outputRow); 764 free(rowBytes); 765 return; 766 } 767 768 for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) { 769 // Read rows sequentially using readNextRow 770 if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) { 771 LOG_ERR("GFX", "Failed to read row %d from 1-bit bitmap", bmpY); 772 free(outputRow); 773 free(rowBytes); 774 return; 775 } 776 777 // Calculate screen Y based on whether BMP is top-down or bottom-up 778 const int bmpYOffset = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY; 779 int screenY = y + (isScaled ? static_cast<int>(std::floor(bmpYOffset * scale)) : bmpYOffset); 780 if (screenY >= getScreenHeight()) { 781 continue; // Continue reading to keep row counter in sync 782 } 783 if (screenY < 0) { 784 continue; 785 } 786 787 for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) { 788 int screenX = x + (isScaled ? static_cast<int>(std::floor(bmpX * scale)) : bmpX); 789 if (screenX >= getScreenWidth()) { 790 break; 791 } 792 if (screenX < 0) { 793 continue; 794 } 795 796 // Get 2-bit value (result of readNextRow quantization) 797 const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3; 798 799 // For 1-bit source: 0 or 1 -> map to black (0,1,2) or white (3) 800 // val < 3 means black pixel (draw it) 801 if (val < 3) { 802 drawPixel(screenX, screenY, true); 803 } 804 // White pixels (val == 3) are not drawn (leave background) 805 } 806 } 807 808 free(outputRow); 809 free(rowBytes); 810} 811 812void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state) const { 813 if (numPoints < 3) return; 814 815 // Find bounding box 816 int minY = yPoints[0], maxY = yPoints[0]; 817 for (int i = 1; i < numPoints; i++) { 818 if (yPoints[i] < minY) minY = yPoints[i]; 819 if (yPoints[i] > maxY) maxY = yPoints[i]; 820 } 821 822 // Clip to screen 823 if (minY < 0) minY = 0; 824 if (maxY >= getScreenHeight()) maxY = getScreenHeight() - 1; 825 826 // Allocate node buffer for scanline algorithm 827 auto* nodeX = static_cast<int*>(malloc(numPoints * sizeof(int))); 828 if (!nodeX) { 829 LOG_ERR("GFX", "!! Failed to allocate polygon node buffer"); 830 return; 831 } 832 833 // Scanline fill algorithm 834 for (int scanY = minY; scanY <= maxY; scanY++) { 835 int nodes = 0; 836 837 // Find all intersection points with edges 838 int j = numPoints - 1; 839 for (int i = 0; i < numPoints; i++) { 840 if ((yPoints[i] < scanY && yPoints[j] >= scanY) || (yPoints[j] < scanY && yPoints[i] >= scanY)) { 841 // Calculate X intersection using fixed-point to avoid float 842 int dy = yPoints[j] - yPoints[i]; 843 if (dy != 0) { 844 nodeX[nodes++] = xPoints[i] + (scanY - yPoints[i]) * (xPoints[j] - xPoints[i]) / dy; 845 } 846 } 847 j = i; 848 } 849 850 // Sort nodes by X (simple bubble sort, numPoints is small) 851 for (int i = 0; i < nodes - 1; i++) { 852 for (int k = i + 1; k < nodes; k++) { 853 if (nodeX[i] > nodeX[k]) { 854 int temp = nodeX[i]; 855 nodeX[i] = nodeX[k]; 856 nodeX[k] = temp; 857 } 858 } 859 } 860 861 // Fill between pairs of nodes 862 for (int i = 0; i < nodes - 1; i += 2) { 863 int startX = nodeX[i]; 864 int endX = nodeX[i + 1]; 865 866 // Clip to screen 867 if (startX < 0) startX = 0; 868 if (endX >= getScreenWidth()) endX = getScreenWidth() - 1; 869 870 // Draw horizontal line 871 for (int x = startX; x <= endX; x++) { 872 drawPixel(x, scanY, state); 873 } 874 } 875 } 876 877 free(nodeX); 878} 879 880// For performance measurement (using static to allow "const" methods) 881static unsigned long start_ms = 0; 882 883void GfxRenderer::clearScreen(const uint8_t color) const { 884 start_ms = millis(); 885 display.clearScreen(color); 886} 887 888void GfxRenderer::invertScreen() const { 889 for (uint32_t i = 0; i < frameBufferSize; i++) { 890 frameBuffer[i] = ~frameBuffer[i]; 891 } 892} 893 894void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const { 895 auto elapsed = millis() - start_ms; 896 LOG_DBG("GFX", "Time = %lu ms from clearScreen to displayBuffer", elapsed); 897 display.displayBuffer(refreshMode, fadingFix); 898} 899 900std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, 901 const EpdFontFamily::Style style) const { 902 if (!text || maxWidth <= 0) return ""; 903 904 std::string item = text; 905 // U+2026 HORIZONTAL ELLIPSIS (UTF-8: 0xE2 0x80 0xA6) 906 const char* ellipsis = "\xe2\x80\xa6"; 907 int textWidth = getTextWidth(fontId, item.c_str(), style); 908 if (textWidth <= maxWidth) { 909 // Text fits, return as is 910 return item; 911 } 912 913 while (!item.empty() && getTextWidth(fontId, (item + ellipsis).c_str(), style) >= maxWidth) { 914 utf8RemoveLastChar(item); 915 } 916 917 return item.empty() ? ellipsis : item + ellipsis; 918} 919 920std::vector<std::string> GfxRenderer::wrappedText(const int fontId, const char* text, const int maxWidth, 921 const int maxLines, const EpdFontFamily::Style style) const { 922 std::vector<std::string> lines; 923 924 if (!text || maxWidth <= 0 || maxLines <= 0) return lines; 925 926 std::string remaining = text; 927 std::string currentLine; 928 929 while (!remaining.empty()) { 930 if (static_cast<int>(lines.size()) == maxLines - 1) { 931 // Last available line: combine any word already started on this line with 932 // the rest of the text, then let truncatedText fit it with an ellipsis. 933 std::string lastContent = currentLine.empty() ? remaining : currentLine + " " + remaining; 934 lines.push_back(truncatedText(fontId, lastContent.c_str(), maxWidth, style)); 935 return lines; 936 } 937 938 // Find next word 939 size_t spacePos = remaining.find(' '); 940 std::string word; 941 942 if (spacePos == std::string::npos) { 943 word = remaining; 944 remaining.clear(); 945 } else { 946 word = remaining.substr(0, spacePos); 947 remaining.erase(0, spacePos + 1); 948 } 949 950 std::string testLine = currentLine.empty() ? word : currentLine + " " + word; 951 952 if (getTextWidth(fontId, testLine.c_str(), style) <= maxWidth) { 953 currentLine = testLine; 954 } else { 955 if (!currentLine.empty()) { 956 lines.push_back(currentLine); 957 // If the carried-over word itself exceeds maxWidth, truncate it and 958 // push it as a complete line immediately — storing it in currentLine 959 // would allow a subsequent short word to be appended after the ellipsis. 960 if (getTextWidth(fontId, word.c_str(), style) > maxWidth) { 961 lines.push_back(truncatedText(fontId, word.c_str(), maxWidth, style)); 962 currentLine.clear(); 963 if (static_cast<int>(lines.size()) >= maxLines) return lines; 964 } else { 965 currentLine = word; 966 } 967 } else { 968 // Single word wider than maxWidth: truncate and stop to avoid complicated 969 // splitting rules (different between languages). Results in an aesthetically 970 // pleasing end. 971 lines.push_back(truncatedText(fontId, word.c_str(), maxWidth, style)); 972 return lines; 973 } 974 } 975 } 976 977 if (!currentLine.empty() && static_cast<int>(lines.size()) < maxLines) { 978 lines.push_back(currentLine); 979 } 980 981 return lines; 982} 983 984// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation 985int GfxRenderer::getScreenWidth() const { 986 switch (orientation) { 987 case Portrait: 988 case PortraitInverted: 989 // 480px wide in portrait logical coordinates 990 return panelHeight; 991 case LandscapeClockwise: 992 case LandscapeCounterClockwise: 993 // 800px wide in landscape logical coordinates 994 return panelWidth; 995 } 996 return panelHeight; 997} 998 999int GfxRenderer::getScreenHeight() const { 1000 switch (orientation) { 1001 case Portrait: 1002 case PortraitInverted: 1003 // 800px tall in portrait logical coordinates 1004 return panelWidth; 1005 case LandscapeClockwise: 1006 case LandscapeCounterClockwise: 1007 // 480px tall in landscape logical coordinates 1008 return panelHeight; 1009 } 1010 return panelWidth; 1011} 1012 1013int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style style) const { 1014 const auto fontIt = fontMap.find(fontId); 1015 if (fontIt == fontMap.end()) { 1016 LOG_ERR("GFX", "Font %d not found", fontId); 1017 return 0; 1018 } 1019 1020 const EpdGlyph* spaceGlyph = fontIt->second.getGlyph(' ', style); 1021 return spaceGlyph ? fp4::toPixel(spaceGlyph->advanceX) : 0; // snap 12.4 fixed-point to nearest pixel 1022} 1023 1024int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const uint32_t rightCp, 1025 const EpdFontFamily::Style style) const { 1026 const auto fontIt = fontMap.find(fontId); 1027 if (fontIt == fontMap.end()) return 0; 1028 const auto& font = fontIt->second; 1029 const EpdGlyph* spaceGlyph = font.getGlyph(' ', style); 1030 const int32_t spaceAdvanceFP = spaceGlyph ? static_cast<int32_t>(spaceGlyph->advanceX) : 0; 1031 // Combine space advance + flanking kern into one fixed-point sum before snapping. 1032 // Snapping the combined value avoids the +/-1 px error from snapping each component separately. 1033 const int32_t kernFP = static_cast<int32_t>(font.getKerning(leftCp, ' ', style)) + 1034 static_cast<int32_t>(font.getKerning(' ', rightCp, style)); 1035 return fp4::toPixel(spaceAdvanceFP + kernFP); 1036} 1037 1038int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint32_t rightCp, 1039 const EpdFontFamily::Style style) const { 1040 const auto fontIt = fontMap.find(fontId); 1041 if (fontIt == fontMap.end()) return 0; 1042 const int kernFP = fontIt->second.getKerning(leftCp, rightCp, style); // 4.4 fixed-point 1043 return fp4::toPixel(kernFP); // snap 4.4 fixed-point to nearest pixel 1044} 1045 1046int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const { 1047 const auto fontIt = fontMap.find(fontId); 1048 if (fontIt == fontMap.end()) { 1049 LOG_ERR("GFX", "Font %d not found", fontId); 1050 return 0; 1051 } 1052 1053 uint32_t cp; 1054 uint32_t prevCp = 0; 1055 int widthPx = 0; 1056 int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap 1057 const auto& font = fontIt->second; 1058 while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) { 1059 if (utf8IsCombiningMark(cp)) { 1060 continue; 1061 } 1062 cp = font.applyLigatures(cp, text, style); 1063 1064 // Differential rounding: snap (previous advance + current kern) together, 1065 // matching drawText so measurement and rendering agree exactly. 1066 if (prevCp != 0) { 1067 const auto kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern 1068 widthPx += fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel 1069 } 1070 1071 const EpdGlyph* glyph = font.getGlyph(cp, style); 1072 prevAdvanceFP = glyph ? glyph->advanceX : 0; 1073 prevCp = cp; 1074 } 1075 widthPx += fp4::toPixel(prevAdvanceFP); // final glyph's advance 1076 return widthPx; 1077} 1078 1079int GfxRenderer::getFontAscenderSize(const int fontId) const { 1080 const auto fontIt = fontMap.find(fontId); 1081 if (fontIt == fontMap.end()) { 1082 LOG_ERR("GFX", "Font %d not found", fontId); 1083 return 0; 1084 } 1085 1086 return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender; 1087} 1088 1089int GfxRenderer::getLineHeight(const int fontId) const { 1090 const auto fontIt = fontMap.find(fontId); 1091 if (fontIt == fontMap.end()) { 1092 LOG_ERR("GFX", "Font %d not found", fontId); 1093 return 0; 1094 } 1095 1096 return fontIt->second.getData(EpdFontFamily::REGULAR)->advanceY; 1097} 1098 1099int GfxRenderer::getTextHeight(const int fontId) const { 1100 const auto fontIt = fontMap.find(fontId); 1101 if (fontIt == fontMap.end()) { 1102 LOG_ERR("GFX", "Font %d not found", fontId); 1103 return 0; 1104 } 1105 return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender; 1106} 1107 1108void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black, 1109 const EpdFontFamily::Style style) const { 1110 // Cannot draw a NULL / empty string 1111 if (text == nullptr || *text == '\0') { 1112 return; 1113 } 1114 1115 const auto fontIt = fontMap.find(fontId); 1116 if (fontIt == fontMap.end()) { 1117 LOG_ERR("GFX", "Font %d not found", fontId); 1118 return; 1119 } 1120 1121 const auto& font = fontIt->second; 1122 1123 int lastBaseY = y; 1124 int lastBaseLeft = 0; 1125 int lastBaseWidth = 0; 1126 int lastBaseTop = 0; 1127 int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap 1128 1129 uint32_t cp; 1130 uint32_t prevCp = 0; 1131 while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) { 1132 if (utf8IsCombiningMark(cp)) { 1133 const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); 1134 if (!combiningGlyph) continue; 1135 const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop); 1136 const int combiningX = x - raiseBy; 1137 const int combiningY = combiningMark::centerOverRotated90CW(lastBaseY, lastBaseLeft, lastBaseWidth, 1138 combiningGlyph->left, combiningGlyph->width); 1139 renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style); 1140 continue; 1141 } 1142 1143 cp = font.applyLigatures(cp, text, style); 1144 1145 // Differential rounding: snap (previous advance + current kern) as one unit, 1146 // subtracting for the rotated coordinate direction. 1147 if (prevCp != 0) { 1148 const auto kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern 1149 lastBaseY -= fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel 1150 } 1151 1152 const EpdGlyph* glyph = font.getGlyph(cp, style); 1153 1154 lastBaseLeft = glyph ? glyph->left : 0; 1155 lastBaseWidth = glyph ? glyph->width : 0; 1156 lastBaseTop = glyph ? glyph->top : 0; 1157 prevAdvanceFP = glyph ? glyph->advanceX : 0; // 12.4 fixed-point 1158 1159 renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, x, lastBaseY, black, style); 1160 prevCp = cp; 1161 } 1162} 1163 1164uint8_t* GfxRenderer::getFrameBuffer() const { return frameBuffer; } 1165 1166size_t GfxRenderer::getBufferSize() const { return frameBufferSize; } 1167 1168// unused 1169// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); } 1170 1171void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(frameBuffer); } 1172 1173void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(frameBuffer); } 1174 1175void GfxRenderer::displayGrayBuffer() const { display.displayGrayBuffer(fadingFix); } 1176 1177void GfxRenderer::freeBwBufferChunks() { 1178 for (auto& bwBufferChunk : bwBufferChunks) { 1179 if (bwBufferChunk) { 1180 free(bwBufferChunk); 1181 bwBufferChunk = nullptr; 1182 } 1183 } 1184} 1185 1186/** 1187 * This should be called before grayscale buffers are populated. 1188 * A `restoreBwBuffer` call should always follow the grayscale render if this method was called. 1189 * Uses chunked allocation to avoid needing 48KB of contiguous memory. 1190 * Returns true if buffer was stored successfully, false if allocation failed. 1191 */ 1192bool GfxRenderer::storeBwBuffer() { 1193 // Allocate and copy each chunk 1194 for (size_t i = 0; i < bwBufferChunks.size(); i++) { 1195 // Check if any chunks are already allocated 1196 if (bwBufferChunks[i]) { 1197 LOG_ERR("GFX", "!! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk", i); 1198 free(bwBufferChunks[i]); 1199 bwBufferChunks[i] = nullptr; 1200 } 1201 1202 const size_t offset = i * BW_BUFFER_CHUNK_SIZE; 1203 const size_t chunkSize = std::min(BW_BUFFER_CHUNK_SIZE, static_cast<size_t>(frameBufferSize - offset)); 1204 bwBufferChunks[i] = static_cast<uint8_t*>(malloc(chunkSize)); 1205 1206 if (!bwBufferChunks[i]) { 1207 LOG_ERR("GFX", "!! Failed to allocate BW buffer chunk %zu (%zu bytes)", i, chunkSize); 1208 // Free previously allocated chunks 1209 freeBwBufferChunks(); 1210 return false; 1211 } 1212 1213 memcpy(bwBufferChunks[i], frameBuffer + offset, chunkSize); 1214 } 1215 1216 LOG_DBG("GFX", "Stored BW buffer in %zu chunks (%zu bytes each)", bwBufferChunks.size(), BW_BUFFER_CHUNK_SIZE); 1217 return true; 1218} 1219 1220/** 1221 * This can only be called if `storeBwBuffer` was called prior to the grayscale render. 1222 * It should be called to restore the BW buffer state after grayscale rendering is complete. 1223 * Uses chunked restoration to match chunked storage. 1224 */ 1225void GfxRenderer::restoreBwBuffer() { 1226 // Check if all chunks are allocated 1227 bool missingChunks = false; 1228 for (const auto& bwBufferChunk : bwBufferChunks) { 1229 if (!bwBufferChunk) { 1230 missingChunks = true; 1231 break; 1232 } 1233 } 1234 1235 if (missingChunks) { 1236 freeBwBufferChunks(); 1237 return; 1238 } 1239 1240 for (size_t i = 0; i < bwBufferChunks.size(); i++) { 1241 const size_t offset = i * BW_BUFFER_CHUNK_SIZE; 1242 const size_t chunkSize = std::min(BW_BUFFER_CHUNK_SIZE, static_cast<size_t>(frameBufferSize - offset)); 1243 memcpy(frameBuffer + offset, bwBufferChunks[i], chunkSize); 1244 } 1245 1246 display.cleanupGrayscaleBuffers(frameBuffer); 1247 1248 freeBwBufferChunks(); 1249 LOG_DBG("GFX", "Restored and freed BW buffer chunks"); 1250} 1251 1252/** 1253 * Cleanup grayscale buffers using the current frame buffer. 1254 * Use this when BW buffer was re-rendered instead of stored/restored. 1255 */ 1256void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const { 1257 if (frameBuffer) { 1258 display.cleanupGrayscaleBuffers(frameBuffer); 1259 } 1260} 1261 1262void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const { 1263 switch (orientation) { 1264 case Portrait: 1265 *outTop = VIEWABLE_MARGIN_TOP; 1266 *outRight = VIEWABLE_MARGIN_RIGHT; 1267 *outBottom = VIEWABLE_MARGIN_BOTTOM; 1268 *outLeft = VIEWABLE_MARGIN_LEFT; 1269 break; 1270 case LandscapeClockwise: 1271 *outTop = VIEWABLE_MARGIN_LEFT; 1272 *outRight = VIEWABLE_MARGIN_TOP; 1273 *outBottom = VIEWABLE_MARGIN_RIGHT; 1274 *outLeft = VIEWABLE_MARGIN_BOTTOM; 1275 break; 1276 case PortraitInverted: 1277 *outTop = VIEWABLE_MARGIN_BOTTOM; 1278 *outRight = VIEWABLE_MARGIN_LEFT; 1279 *outBottom = VIEWABLE_MARGIN_TOP; 1280 *outLeft = VIEWABLE_MARGIN_RIGHT; 1281 break; 1282 case LandscapeCounterClockwise: 1283 *outTop = VIEWABLE_MARGIN_RIGHT; 1284 *outRight = VIEWABLE_MARGIN_BOTTOM; 1285 *outBottom = VIEWABLE_MARGIN_LEFT; 1286 *outLeft = VIEWABLE_MARGIN_TOP; 1287 break; 1288 } 1289}