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.

feat: optimize fillRectDither (#737)

## Summary

This PR optimizes the `fillRectDither` function, making it as fast as a
normal `fillRect`

Testing code:

```cpp
{
auto start_t = millis();
renderer.fillRectDither(0, 0, renderer.getScreenWidth(), renderer.getScreenHeight(), Color::LightGray);
auto elapsed = millis() - start_t;
Serial.printf("[%lu] [ ] Test fillRectDither drawn in %lu ms\n", millis(), elapsed);
}

{
auto start_t = millis();
renderer.fillRect(0, 0, renderer.getScreenWidth(), renderer.getScreenHeight(), true);
auto elapsed = millis() - start_t;
Serial.printf("[%lu] [ ] Test fillRect drawn in %lu ms\n", millis(), elapsed);
}
```

Before:

```
[1125] [ ] Test fillRectDither drawn in 327 ms
[1347] [ ] Test fillRect drawn in 222 ms
```

After:

```
[1065] [ ] Test fillRectDither drawn in 238 ms
[1287] [ ] Test fillRect drawn in 222 ms
```

## Visual validation

Before:

<img width="415" height="216" alt="Screenshot 2026-02-07 at 01 04 19"
src="https://github.com/user-attachments/assets/5802dbba-187b-4d2b-a359-1318d3932d38"
/>

After:

<img width="420" height="191" alt="Screenshot 2026-02-07 at 01 36 30"
src="https://github.com/user-attachments/assets/3c3c8e14-3f3a-4205-be78-6ed771dcddf4"
/>

## Details

The original version is quite slow because it does quite a lot of
computations. A single pixel needs around 20 instructions just to know
if it's black or white:

<img width="1170" height="693" alt="Screenshot 2026-02-07 at 00 15 54"
src="https://github.com/user-attachments/assets/7c5a55e7-0598-4340-8b7b-17307d7921cb"
/>

With the new, templated and more light-weight approach, each pixel takes
only 3-4 instructions, the modulo operator is translated into bitwise
ops:

<img width="1175" height="682" alt="Screenshot 2026-02-07 at 01 47 51"
src="https://github.com/user-attachments/assets/4ec2cf74-6cc0-4b5b-87d5-831563ef164f"
/>

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**

authored by

Xuan-Son Nguyen and committed by
GitHub
76908d38 e6f5fa43

+61 -38
+57 -36
lib/GfxRenderer/GfxRenderer.cpp
··· 237 237 } 238 238 } 239 239 240 - static constexpr uint8_t bayer4x4[4][4] = { 241 - {0, 8, 2, 10}, 242 - {12, 4, 14, 6}, 243 - {3, 11, 1, 9}, 244 - {15, 7, 13, 5}, 245 - }; 246 - static constexpr int matrixSize = 4; 247 - static constexpr int matrixLevels = matrixSize * matrixSize; 240 + // NOTE: Those are in critical path, and need to be templated to avoid runtime checks for every pixel. 241 + // Any branching must be done outside the loops to avoid performance degradation. 242 + template <> 243 + void GfxRenderer::drawPixelDither<Color::Clear>(const int x, const int y) const { 244 + // Do nothing 245 + } 246 + 247 + template <> 248 + void GfxRenderer::drawPixelDither<Color::Black>(const int x, const int y) const { 249 + drawPixel(x, y, true); 250 + } 251 + 252 + template <> 253 + void GfxRenderer::drawPixelDither<Color::White>(const int x, const int y) const { 254 + drawPixel(x, y, false); 255 + } 248 256 249 - void GfxRenderer::drawPixelDither(const int x, const int y, Color color) const { 250 - if (color == Color::Clear) { 251 - } else if (color == Color::Black) { 252 - drawPixel(x, y, true); 253 - } else if (color == Color::White) { 254 - drawPixel(x, y, false); 255 - } else { 256 - // Use dithering 257 - const int greyLevel = static_cast<int>(color) - 1; // 0-15 258 - const int normalizedGrey = (greyLevel * 255) / (matrixLevels - 1); 259 - const int clampedGrey = std::max(0, std::min(normalizedGrey, 255)); 260 - const int threshold = (clampedGrey * (matrixLevels + 1)) / 256; 257 + template <> 258 + void GfxRenderer::drawPixelDither<Color::LightGray>(const int x, const int y) const { 259 + drawPixel(x, y, x % 2 == 0 && y % 2 == 0); 260 + } 261 261 262 - const int matrixX = x & (matrixSize - 1); 263 - const int matrixY = y & (matrixSize - 1); 264 - const uint8_t patternValue = bayer4x4[matrixY][matrixX]; 265 - const bool black = patternValue < threshold; 266 - drawPixel(x, y, black); 267 - } 262 + template <> 263 + void GfxRenderer::drawPixelDither<Color::DarkGray>(const int x, const int y) const { 264 + drawPixel(x, y, (x + y) % 2 == 0); // TODO: maybe find a better pattern? 268 265 } 269 266 270 - // Use Bayer matrix 4x4 dithering to fill the rectangle with a grey level 271 267 void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const { 272 268 if (color == Color::Clear) { 273 269 } else if (color == Color::Black) { 274 270 fillRect(x, y, width, height, true); 275 271 } else if (color == Color::White) { 276 272 fillRect(x, y, width, height, false); 277 - } else { 273 + } else if (color == Color::LightGray) { 278 274 for (int fillY = y; fillY < y + height; fillY++) { 279 275 for (int fillX = x; fillX < x + width; fillX++) { 280 - drawPixelDither(fillX, fillY, color); 276 + drawPixelDither<Color::LightGray>(fillX, fillY); 277 + } 278 + } 279 + } else if (color == Color::DarkGray) { 280 + for (int fillY = y; fillY < y + height; fillY++) { 281 + for (int fillX = x; fillX < x + width; fillX++) { 282 + drawPixelDither<Color::DarkGray>(fillX, fillY); 281 283 } 282 284 } 283 285 } 284 286 } 285 287 286 - void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir, 287 - Color color) const { 288 + template <Color color> 289 + void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir) const { 288 290 const int radiusSq = maxRadius * maxRadius; 289 291 for (int dy = 0; dy <= maxRadius; ++dy) { 290 292 for (int dx = 0; dx <= maxRadius; ++dx) { ··· 292 294 const int px = cx + xDir * dx; 293 295 const int py = cy + yDir * dy; 294 296 if (distSq <= radiusSq) { 295 - drawPixelDither(px, py, color); 297 + drawPixelDither<color>(px, py); 296 298 } 297 299 } 298 300 } ··· 327 329 fillRectDither(x + width - maxRadius - 1, y + maxRadius + 1, maxRadius + 1, verticalHeight, color); 328 330 } 329 331 332 + auto fillArcTemplated = [this](int maxRadius, int cx, int cy, int xDir, int yDir, Color color) { 333 + switch (color) { 334 + case Color::Clear: 335 + break; 336 + case Color::Black: 337 + fillArc<Color::Black>(maxRadius, cx, cy, xDir, yDir); 338 + break; 339 + case Color::White: 340 + fillArc<Color::White>(maxRadius, cx, cy, xDir, yDir); 341 + break; 342 + case Color::LightGray: 343 + fillArc<Color::LightGray>(maxRadius, cx, cy, xDir, yDir); 344 + break; 345 + case Color::DarkGray: 346 + fillArc<Color::DarkGray>(maxRadius, cx, cy, xDir, yDir); 347 + break; 348 + } 349 + }; 350 + 330 351 if (roundTopLeft) { 331 - fillArc(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color); 352 + fillArcTemplated(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color); 332 353 } else { 333 354 fillRectDither(x, y, maxRadius + 1, maxRadius + 1, color); 334 355 } 335 356 336 357 if (roundTopRight) { 337 - fillArc(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color); 358 + fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color); 338 359 } else { 339 360 fillRectDither(x + width - maxRadius - 1, y, maxRadius + 1, maxRadius + 1, color); 340 361 } 341 362 342 363 if (roundBottomRight) { 343 - fillArc(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color); 364 + fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color); 344 365 } else { 345 366 fillRectDither(x + width - maxRadius - 1, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color); 346 367 } 347 368 348 369 if (roundBottomLeft) { 349 - fillArc(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color); 370 + fillArcTemplated(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color); 350 371 } else { 351 372 fillRectDither(x, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color); 352 373 }
+4 -2
lib/GfxRenderer/GfxRenderer.h
··· 39 39 EpdFontFamily::Style style) const; 40 40 void freeBwBufferChunks(); 41 41 void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const; 42 - void drawPixelDither(int x, int y, Color color) const; 43 - void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir, Color color) const; 42 + template <Color color> 43 + void drawPixelDither(int x, int y) const; 44 + template <Color color> 45 + void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const; 44 46 45 47 public: 46 48 explicit GfxRenderer(HalDisplay& halDisplay)