A fork of https://github.com/crosspoint-reader/crosspoint-reader
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}